tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from dateutil.tz import tzlocal
  41from time import sleep
  42
  43import re
  44import json
  45import requests
  46import traceback as tb
  47from typing import Union
  48
  49from multiprocessing import cpu_count, Lock
  50from multiprocessing.pool import ThreadPool
  51import pandas as pd
  52
  53from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
  54from Templates import *  # Some html-templates used by reporting methods in TKSBrokerAPI module
  55from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  56from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  57
  58from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
  59from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  60
  61import UniLogger as uLog  # Logger for TKSBrokerAPI
  62
  63
  64# --- Common technical parameters:
  65
  66PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  67uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  68uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  69uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  70
  71__version__ = "1.6"  # The "major.minor" version setup here, but build number define at the build-server only
  72
  73CPU_COUNT = cpu_count()  # host's real CPU count
  74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  75
  76
  77class TinkoffBrokerServer:
  78    """
  79    This class implements methods to work with Tinkoff broker server.
  80
  81    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  82
  83    About `token`: https://tinkoff.github.io/investAPI/token/
  84    """
  85    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  86        """
  87        Main class init.
  88
  89        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  90        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  91                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  92        :param useCache: use default cache file with raw data to use instead of `iList`.
  93                         True by default. Cache is auto-update if new day has come.
  94                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  95        :param defaultCache: path to default cache file. `dump.json` by default.
  96        """
  97        if token is None or not token:
  98            try:
  99                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 100                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 101
 102            except KeyError:
 103                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 104                raise Exception("Token required")
 105
 106        else:
 107            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 108            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 109
 110        if accountId is None or not accountId:
 111            try:
 112                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 113                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 114
 115            except KeyError:
 116                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 117
 118        else:
 119            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 120            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 121
 122        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 123        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 124
 125        Latest version: https://pypi.org/project/tksbrokerapi/
 126        """
 127
 128        self.__lock = Lock()  # initialize multiprocessing mutex lock
 129
 130        self.aliases = TKS_TICKER_ALIASES
 131        """Some aliases instead official tickers.
 132
 133        See also: `TKSEnums.TKS_TICKER_ALIASES`
 134        """
 135
 136        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 137
 138        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 139
 140        self._ticker = ""
 141        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 142
 143        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 144        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 145
 146        See also: `SearchByTicker()`, `SearchInstruments()`.
 147        """
 148
 149        self._figi = ""
 150        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 151
 152        See also: `SearchByFIGI()`, `SearchInstruments()`.
 153        """
 154
 155        self.depth = 1
 156        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 157
 158        See also: `GetCurrentPrices()`.
 159        """
 160
 161        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 162        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 163
 164        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 165        """
 166
 167        uLogger.debug("Broker API server: {}".format(self.server))
 168
 169        self.timeout = 15
 170        """Server operations timeout in seconds. Default: `15`.
 171
 172        See also: `SendAPIRequest()`.
 173        """
 174
 175        self.headers = {
 176            "Content-Type": "application/json",
 177            "accept": "application/json",
 178            "Authorization": "Bearer {}".format(self.token),
 179            "x-app-name": "Tim55667757.TKSBrokerAPI",
 180        }
 181        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 182
 183        See also: `SendAPIRequest()`.
 184        """
 185
 186        self.body = None
 187        """Request body which send to broker server. Default: `None`.
 188
 189        See also: `SendAPIRequest()`.
 190        """
 191
 192        self.moreDebug = False
 193        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 194
 195        self.useHTMLReports = False
 196        """
 197        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 198        
 199        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 200        """
 201
 202        self.historyFile = None
 203        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 204
 205        See also: `History()`.
 206        """
 207
 208        self.htmlHistoryFile = "index.html"
 209        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 210
 211        See also: `ShowHistoryChart()`.
 212        """
 213
 214        self.instrumentsFile = "instruments.md"
 215        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 216
 217        See also: `ShowInstrumentsInfo()`.
 218        """
 219
 220        self.searchResultsFile = "search-results.md"
 221        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 222
 223        See also: `SearchInstruments()`.
 224        """
 225
 226        self.pricesFile = "prices.md"
 227        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 228
 229        See also: `GetListOfPrices()`.
 230        """
 231
 232        self.infoFile = "info.md"
 233        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 234
 235        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 236        """
 237
 238        self.bondsXLSXFile = "ext-bonds.xlsx"
 239        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 240        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 241
 242        See also: `ExtendBondsData()`.
 243        """
 244
 245        self.calendarFile = "calendar.md"
 246        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 247        
 248        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 249
 250        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 251        """
 252
 253        self.overviewFile = "overview.md"
 254        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 255
 256        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 257        """
 258
 259        self.overviewDigestFile = "overview-digest.md"
 260        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 261
 262        See also: `Overview()` with parameter `details="digest"`.
 263        """
 264
 265        self.overviewPositionsFile = "overview-positions.md"
 266        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 267
 268        See also: `Overview()` with parameter `details="positions"`.
 269        """
 270
 271        self.overviewOrdersFile = "overview-orders.md"
 272        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 273
 274        See also: `Overview()` with parameter `details="orders"`.
 275        """
 276
 277        self.overviewAnalyticsFile = "overview-analytics.md"
 278        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 279
 280        See also: `Overview()` with parameter `details="analytics"`.
 281        """
 282
 283        self.overviewBondsCalendarFile = "overview-calendar.md"
 284        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 285
 286        See also: `Overview()` with parameter `details="calendar"`.
 287        """
 288
 289        self.reportFile = "deals.md"
 290        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 291
 292        See also: `Deals()`.
 293        """
 294
 295        self.withdrawalLimitsFile = "limits.md"
 296        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 297
 298        See also: `OverviewLimits()` and `RequestLimits()`.
 299        """
 300
 301        self.userInfoFile = "user-info.md"
 302        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 303
 304        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 305        """
 306
 307        self.userAccountsFile = "accounts.md"
 308        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 309
 310        See also: `OverviewAccounts()`, `RequestAccounts()`.
 311        """
 312
 313        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 314        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 315
 316        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 317
 318        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 319        """
 320
 321        self.iList = None  # init iList for raw instruments data
 322        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 323        
 324        See also: `Listing()`, `DumpInstruments()`.
 325        """
 326
 327        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 328        if useCache:
 329            if os.path.exists(self.iListDumpFile):
 330                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 331                curTime = datetime.now(tzutc())
 332
 333                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 334                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 335
 336                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 337
 338                else:
 339                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 340
 341                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 342                        os.path.abspath(self.iListDumpFile),
 343                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 344                    ))
 345
 346            else:
 347                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 348                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 349
 350        else:
 351            self.iList = self.Listing()  # request new raw instruments data from broker server
 352            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 353
 354        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 355        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 356
 357        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 358        """
 359
 360    @property
 361    def ticker(self) -> str:
 362        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 363
 364        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 365        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 366
 367        See also: `SearchByTicker()`, `SearchInstruments()`.
 368        """
 369        return self._ticker
 370
 371    @ticker.setter
 372    def ticker(self, value):
 373        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 374
 375        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 376        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 377
 378        See also: `SearchByTicker()`, `SearchInstruments()`.
 379        """
 380        self._ticker = str(value).upper()  # Tickers may be upper case only
 381
 382    @property
 383    def figi(self) -> str:
 384        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 385
 386        See also: `SearchByFIGI()`, `SearchInstruments()`.
 387        """
 388        return self._figi
 389
 390    @figi.setter
 391    def figi(self, value):
 392        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 393
 394        See also: `SearchByFIGI()`, `SearchInstruments()`.
 395        """
 396        self._figi = str(value).upper()  # FIGI may be upper case only
 397
 398    def _ParseJSON(self, rawData="{}") -> dict:
 399        """
 400        Parse JSON from response string.
 401
 402        :param rawData: this is a string with JSON-formatted text.
 403        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 404        """
 405        try:
 406            responseJSON = json.loads(rawData) if rawData else {}
 407
 408            if self.moreDebug:
 409                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 410
 411            return responseJSON
 412
 413        except Exception as e:
 414            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 415
 416            return {}
 417
 418    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 419        """
 420        Send GET or POST request to broker server and receive JSON object.
 421
 422        self.header: must be defining with dictionary of headers.
 423        self.body: if define then used as request body. None by default.
 424        self.timeout: global request timeout, 15 seconds by default.
 425        :param url: url with REST request.
 426        :param reqType: send "GET" or "POST" request. "GET" by default.
 427        :param retry: how many times retry after first request if an 5xx server errors occurred.
 428        :param pause: sleep time in seconds between retries.
 429        :return: response JSON (dictionary) from broker.
 430        """
 431        if reqType.upper() not in ("GET", "POST"):
 432            uLogger.error("You can define request type: `GET` or `POST`!")
 433            raise Exception("Incorrect value")
 434
 435        if self.moreDebug:
 436            uLogger.debug("Request parameters:")
 437            uLogger.debug("    - REST API URL: {}".format(url))
 438            uLogger.debug("    - request type: {}".format(reqType))
 439            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 440            uLogger.debug("    - body:\n{}".format(self.body))
 441
 442        # fast hack to avoid all operations with some tickers/FIGI
 443        responseJSON = {}
 444        oK = True
 445        for item in self.exclude:
 446            if item in url:
 447                if self.moreDebug:
 448                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 449
 450                oK = False
 451                break
 452
 453        if oK:
 454            with self.__lock:  # acquire the mutex lock
 455                counter = 0
 456                response = None
 457                errMsg = ""
 458
 459                while not response and counter <= retry:
 460                    if reqType == "GET":
 461                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 462
 463                    if reqType == "POST":
 464                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 465
 466                    if self.moreDebug:
 467                        uLogger.debug("Response:")
 468                        uLogger.debug("    - status code: {}".format(response.status_code))
 469                        uLogger.debug("    - reason: {}".format(response.reason))
 470                        uLogger.debug("    - body length: {}".format(len(response.text)))
 471                        uLogger.debug("    - headers:\n{}".format(response.headers))
 472
 473                    # Server returns some headers:
 474                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 475                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 476                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 477                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 478                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 479                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 480                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 481                        sleep(rateLimitWait)
 482
 483                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 484                    if 400 <= response.status_code < 500:
 485                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 486                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 487
 488                        if "code" in response.text and "message" in response.text:
 489                            msgDict = self._ParseJSON(rawData=response.text)
 490                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 491
 492                        counter = retry + 1  # do not retry for 4xx errors
 493
 494                    if 500 <= response.status_code < 600:
 495                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 496                        uLogger.debug("    - not oK, {}".format(errMsg))
 497
 498                        if "code" in response.text and "message" in response.text:
 499                            errMsgDict = self._ParseJSON(rawData=response.text)
 500                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 501
 502                        counter += 1
 503
 504                        if counter <= retry:
 505                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 506                            sleep(pause)
 507
 508                responseJSON = self._ParseJSON(rawData=response.text)
 509
 510                if errMsg:
 511                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 512                    uLogger.error("    - not oK, {}".format(errMsg))
 513
 514        return responseJSON
 515
 516    def _IUpdater(self, iType: str) -> tuple:
 517        """
 518        Request instrument by type from server. See available API methods for instruments:
 519        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 520        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 521        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 522        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 523        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 524
 525        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 526        :return: tuple with iType name and list of available instruments of current type for defined user token.
 527        """
 528        result = []
 529
 530        if iType in TKS_INSTRUMENTS:
 531            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 532
 533            # all instruments have the same body in API v2 requests:
 534            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 535            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 536            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 537
 538        return iType, result
 539
 540    def _IWrapper(self, kwargs):
 541        """
 542        Wrapper runs instrument's update method `_IUpdater()`.
 543        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 544        """
 545        return self._IUpdater(**kwargs)
 546
 547    def Listing(self) -> dict:
 548        """
 549        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 550
 551        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 552        """
 553        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 554        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 555
 556        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 557        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 558        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 559
 560        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 561        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 562        poolUpdater.close()  # close the thread pool
 563        poolUpdater.join()  # wait a moment until all data returns from threads
 564
 565        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 566        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 567        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 568
 569        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 570        for iType in iList.keys():
 571            for ticker in iList[iType]:
 572                iList[iType][ticker]["type"] = iType
 573
 574                if "minPriceIncrement" in iList[iType][ticker].keys():
 575                    iList[iType][ticker]["step"] = NanoToFloat(
 576                        iList[iType][ticker]["minPriceIncrement"]["units"],
 577                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 578                    )
 579
 580                else:
 581                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 582
 583        return iList
 584
 585    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 586        """
 587        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 588
 589        See also: `DumpInstruments()`, `Listing()`.
 590
 591        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 592                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 593        """
 594        if self.iListDumpFile is None or not self.iListDumpFile:
 595            uLogger.error("Output name of dump file must be defined!")
 596            raise Exception("Filename required")
 597
 598        if not self.iList or forceUpdate:
 599            self.iList = self.Listing()
 600
 601        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 602
 603        # Save as XLSX with separated sheets for every type of instruments:
 604        with pd.ExcelWriter(
 605                path=xlsxDumpFile,
 606                date_format=TKS_DATE_FORMAT,
 607                datetime_format=TKS_DATE_TIME_FORMAT,
 608                mode="w",
 609        ) as writer:
 610            for iType in TKS_INSTRUMENTS:
 611                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 612                df = df[sorted(df)]  # sorted by column names
 613                df = df.applymap(
 614                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 615                    na_action="ignore",
 616                )  # converting numbers from nano-type to float in every cell
 617                df.to_excel(
 618                    writer,
 619                    sheet_name=iType,
 620                    encoding="UTF-8",
 621                    freeze_panes=(1, 1),
 622                )  # saving as XLSX-file with freeze first row and column as headers
 623
 624        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 625
 626    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 627        """
 628        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 629        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 630
 631        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 632
 633        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 634                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 635        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 636        """
 637        if self.iListDumpFile is None or not self.iListDumpFile:
 638            uLogger.error("Output name of dump file must be defined!")
 639            raise Exception("Filename required")
 640
 641        if not self.iList or forceUpdate:
 642            self.iList = self.Listing()
 643
 644        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 645        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 646            fH.write(jsonDump)
 647
 648        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 649
 650        return jsonDump
 651
 652    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 653        """
 654        Show information about one instrument defined by json data and prints it in Markdown format.
 655
 656        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 657
 658        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 659        :param show: if `True` then also printing information about instrument and its current price.
 660        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 661        :return: multilines text in Markdown format with information about one instrument.
 662        """
 663        splitLine = "|                                                             |                                                        |\n"
 664        infoText = ""
 665
 666        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 667            info = [
 668                "# Main information\n\n",
 669                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 670                "| Parameters                                                  | Values                                                 |\n",
 671                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 672                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 673                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 674            ]
 675
 676            if "sector" in iJSON.keys() and iJSON["sector"]:
 677                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 678
 679            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 680                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 681
 682            info.extend([
 683                splitLine,
 684                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 685                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 686            ])
 687
 688            if "isin" in iJSON.keys() and iJSON["isin"]:
 689                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 690
 691            if "classCode" in iJSON.keys():
 692                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 693
 694            info.extend([
 695                splitLine,
 696                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 697                splitLine,
 698                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 699                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 700                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 701            ])
 702
 703            if iJSON["figi"]:
 704                self._figi = iJSON["figi"]
 705                iJSON = iJSON | self.RequestTradingStatus()
 706
 707                info.extend([
 708                    splitLine,
 709                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 710                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 711                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 712                ])
 713
 714            info.append(splitLine)
 715
 716            if "type" in iJSON.keys() and iJSON["type"]:
 717                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 718
 719                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 720                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 721
 722            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 723                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 724
 725            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 726                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 727
 728            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 729                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 730
 731            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 732                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 733
 734            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 735                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 736
 737            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 738                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 739
 740            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 741                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 742
 743            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 744                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 745
 746            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 747                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 748
 749            if "currency" in iJSON.keys():
 750                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 751
 752            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 753                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 754
 755            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 756                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 757
 758            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 759                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 760
 761            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 762                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 763
 764            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 765                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 766
 767            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 768                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 769
 770            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 771                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 772
 773            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 774                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 775
 776            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 777                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 778
 779            iExt = None
 780            if iJSON["type"] == "Bonds":
 781                info.extend([
 782                    splitLine,
 783                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 784                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 785                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 786                        iJSON["nominal"]["currency"],
 787                    )),
 788                ])
 789
 790                if "floatingCouponFlag" in iJSON.keys():
 791                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 792
 793                if "amortizationFlag" in iJSON.keys():
 794                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 795
 796                info.append(splitLine)
 797
 798                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 799                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 800
 801                if iJSON["figi"]:
 802                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 803
 804                    info.extend([
 805                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 806                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 807                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 808                    ])
 809
 810                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 811                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 812                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 813                        iJSON["aciValue"]["currency"]
 814                    )))
 815
 816            if "currentPrice" in iJSON.keys():
 817                info.append(splitLine)
 818
 819                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 820                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 821
 822                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 823                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 824                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 825                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 826                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 827
 828                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 829                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 830
 831                info.extend([
 832                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 833                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 834                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 835                    )),
 836                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 837                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 838                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 839                    )),
 840                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 841                        "{:.2f}%{}".format(
 842                            iJSON["currentPrice"]["changes"],
 843                            " ({}{:.2f} {})".format(
 844                                "+" if bondChangesDelta > 0 else "",
 845                                bondChangesDelta,
 846                                aciCurrency
 847                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 848                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 849                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 850                                currency
 851                            ),
 852                        )
 853                    ),
 854                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 855                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 856                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 857                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 858                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 859                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 860                    )),
 861                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 862                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 863                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 864                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 865                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 866                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 867                    )),
 868                ])
 869
 870            if "lot" in iJSON.keys():
 871                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 872
 873            if "step" in iJSON.keys() and iJSON["step"] != 0:
 874                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 875
 876            # Add bond payment calendar:
 877            if iJSON["type"] == "Bonds":
 878                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 879                info.extend(["\n#", strCalendar])
 880
 881            infoText += "".join(info)
 882
 883            if show and not onlyFiles:
 884                uLogger.info("{}".format(infoText))
 885
 886            if self.infoFile is not None and (show or onlyFiles):
 887                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 888                    fH.write(infoText)
 889
 890                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 891
 892                if self.useHTMLReports:
 893                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 894                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 895                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 896
 897                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 898
 899        return infoText
 900
 901    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 902        """
 903        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 904
 905        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 906        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 907        :return: JSON formatted data with information about instrument.
 908        """
 909        tickerJSON = {}
 910        if self.moreDebug:
 911            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 912
 913        if not self._ticker:
 914            uLogger.warning("self._ticker variable is not be empty!")
 915
 916        else:
 917            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 918                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 919                raise Exception("Instrument not allowed")
 920
 921            if not self.iList:
 922                self.iList = self.Listing()
 923
 924            if self._ticker in self.iList["Shares"].keys():
 925                tickerJSON = self.iList["Shares"][self._ticker]
 926                if self.moreDebug:
 927                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 928
 929            elif self._ticker in self.iList["Currencies"].keys():
 930                tickerJSON = self.iList["Currencies"][self._ticker]
 931                if self.moreDebug:
 932                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 933
 934            elif self._ticker in self.iList["Bonds"].keys():
 935                tickerJSON = self.iList["Bonds"][self._ticker]
 936                if self.moreDebug:
 937                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 938
 939            elif self._ticker in self.iList["Etfs"].keys():
 940                tickerJSON = self.iList["Etfs"][self._ticker]
 941                if self.moreDebug:
 942                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 943
 944            elif self._ticker in self.iList["Futures"].keys():
 945                tickerJSON = self.iList["Futures"][self._ticker]
 946                if self.moreDebug:
 947                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 948
 949        if tickerJSON:
 950            self._figi = tickerJSON["figi"]
 951
 952            if requestPrice:
 953                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 954
 955                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 956                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 957
 958                else:
 959                    tickerJSON["currentPrice"]["changes"] = 0
 960
 961            if show:
 962                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 963
 964        else:
 965            if show:
 966                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 967
 968        return tickerJSON
 969
 970    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 971        """
 972        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 973
 974        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 975        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 976        :return: JSON formatted data with information about instrument.
 977        """
 978        figiJSON = {}
 979        if self.moreDebug:
 980            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 981
 982        if not self._figi:
 983            uLogger.warning("self._figi variable is not be empty!")
 984
 985        else:
 986            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 987                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 988                raise Exception("Instrument not allowed")
 989
 990            if not self.iList:
 991                self.iList = self.Listing()
 992
 993            for item in self.iList["Shares"].keys():
 994                if self._figi == self.iList["Shares"][item]["figi"]:
 995                    figiJSON = self.iList["Shares"][item]
 996
 997                    if self.moreDebug:
 998                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
 999
1000                    break
1001
1002            if not figiJSON:
1003                for item in self.iList["Currencies"].keys():
1004                    if self._figi == self.iList["Currencies"][item]["figi"]:
1005                        figiJSON = self.iList["Currencies"][item]
1006
1007                        if self.moreDebug:
1008                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1009
1010                        break
1011
1012            if not figiJSON:
1013                for item in self.iList["Bonds"].keys():
1014                    if self._figi == self.iList["Bonds"][item]["figi"]:
1015                        figiJSON = self.iList["Bonds"][item]
1016
1017                        if self.moreDebug:
1018                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1019
1020                        break
1021
1022            if not figiJSON:
1023                for item in self.iList["Etfs"].keys():
1024                    if self._figi == self.iList["Etfs"][item]["figi"]:
1025                        figiJSON = self.iList["Etfs"][item]
1026
1027                        if self.moreDebug:
1028                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1029
1030                        break
1031
1032            if not figiJSON:
1033                for item in self.iList["Futures"].keys():
1034                    if self._figi == self.iList["Futures"][item]["figi"]:
1035                        figiJSON = self.iList["Futures"][item]
1036
1037                        if self.moreDebug:
1038                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1039
1040                        break
1041
1042        if figiJSON:
1043            self._figi = figiJSON["figi"]
1044            self._ticker = figiJSON["ticker"]
1045
1046            if requestPrice:
1047                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1048
1049                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1050                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1051
1052                else:
1053                    figiJSON["currentPrice"]["changes"] = 0
1054
1055            if show:
1056                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1057
1058        else:
1059            if show:
1060                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1061
1062        return figiJSON
1063
1064    def GetCurrentPrices(self, show: bool = True) -> dict:
1065        """
1066        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1067        `{"buy": [{"price": 1243.8, "quantity": 193},
1068                  {"price": 1244.0, "quantity": 168},
1069                  {"price": 1244.8, "quantity": 5},
1070                  {"price": 1245.0, "quantity": 61},
1071                  {"price": 1245.4, "quantity": 60}],
1072          "sell": [{"price": 1243.6, "quantity": 8},
1073                   {"price": 1242.6, "quantity": 10},
1074                   {"price": 1242.4, "quantity": 18},
1075                   {"price": 1242.2, "quantity": 50},
1076                   {"price": 1242.0, "quantity": 113}],
1077          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1078        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1079        - sell: list of dicts with Buyers prices,
1080            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1081            - quantity: volume value by current price in lots,
1082        - limitUp: current trade session limit price, maximum,
1083        - limitDown: current trade session limit price, minimum,
1084        - lastPrice: last deal price of the instrument,
1085        - closePrice: previous trade session close price of the instrument.
1086
1087        See also: `SearchByTicker()` and `SearchByFIGI()`.
1088        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1089        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1090
1091        :param show: if `True` then print DOM to log and console.
1092        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1093                 If an error occurred then returns an empty record:
1094                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1095        """
1096        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1097
1098        if self.depth < 1:
1099            uLogger.error("Depth of Market (DOM) must be >=1!")
1100            raise Exception("Incorrect value")
1101
1102        if not (self._ticker or self._figi):
1103            uLogger.error("self._ticker or self._figi variables must be defined!")
1104            raise Exception("Ticker or FIGI required")
1105
1106        if self._ticker and not self._figi:
1107            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1108            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1109
1110        if not self._ticker and self._figi:
1111            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1112            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1113
1114        if not self._figi:
1115            uLogger.error("FIGI is not defined!")
1116            raise Exception("Ticker or FIGI required")
1117
1118        else:
1119            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1120
1121            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1122            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1123            self.body = str({"figi": self._figi, "depth": self.depth})
1124            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1125
1126            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1127                # list of dicts with sellers orders:
1128                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1129
1130                # list of dicts with buyers orders:
1131                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1132
1133                # max price of instrument at this time:
1134                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1135
1136                # min price of instrument at this time:
1137                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1138
1139                # last price of deal with instrument:
1140                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1141
1142                # last close price of instrument:
1143                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1144
1145            else:
1146                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1147                uLogger.debug("Server response: {}".format(pricesResponse))
1148
1149            if show:
1150                if prices["buy"] or prices["sell"]:
1151                    info = [
1152                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1153                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1154                            self._ticker,
1155                            self._figi,
1156                            self.depth,
1157                        ),
1158                        "-" * 60, "\n",
1159                        "             Orders of Buyers | Orders of Sellers\n",
1160                        "-" * 60, "\n",
1161                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1162                        "-" * 60, "\n",
1163                    ]
1164
1165                    if not prices["buy"]:
1166                        info.append("                              | No orders!\n")
1167                        sumBuy = 0
1168
1169                    else:
1170                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1171                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1172                        for item in maxMinSorted:
1173                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1174
1175                    if not prices["sell"]:
1176                        info.append("No orders!                    |\n")
1177                        sumSell = 0
1178
1179                    else:
1180                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1181                        for item in prices["sell"]:
1182                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1183
1184                    info.extend([
1185                        "-" * 60, "\n",
1186                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1187                        "-" * 60, "\n",
1188                    ])
1189
1190                    infoText = "".join(info)
1191
1192                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1193
1194                else:
1195                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1196
1197        return prices
1198
1199    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1200        """
1201        This method get and show information about all available broker instruments for current user account.
1202        If `instrumentsFile` string is not empty then also save information to this file.
1203
1204        :param show: if `True` then print results to console, if `False` — print only to file.
1205        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1206        :return: multi-lines string with all available broker instruments.
1207        """
1208        if not self.iList:
1209            self.iList = self.Listing()
1210
1211        info = [
1212            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1213            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1214        ]
1215
1216        # add instruments count by type:
1217        for iType in self.iList.keys():
1218            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1219
1220        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1221        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1222
1223        # generating info tables with all instruments by type:
1224        for iType in self.iList.keys():
1225            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1226
1227            for instrument in self.iList[iType].keys():
1228                iName = self.iList[iType][instrument]["name"]  # instrument's name
1229                if len(iName) > 57:
1230                    iName = "{}...".format(iName[:54])  # right trim for a long string
1231
1232                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1233                    self.iList[iType][instrument]["ticker"],
1234                    iName,
1235                    self.iList[iType][instrument]["figi"],
1236                    self.iList[iType][instrument]["currency"],
1237                    self.iList[iType][instrument]["lot"],
1238                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1239                ))
1240
1241        infoText = "".join(info)
1242
1243        if show and not onlyFiles:
1244            uLogger.info(infoText)
1245
1246        if self.instrumentsFile and (show or onlyFiles):
1247            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1248                fH.write(infoText)
1249
1250            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1251
1252            if self.useHTMLReports:
1253                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1254                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1255                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1256
1257                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1258
1259        return infoText
1260
1261    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1262        """
1263        This method search and show information about instruments by part of its ticker, FIGI or name.
1264        If `searchResultsFile` string is not empty then also save information to this file.
1265
1266        :param pattern: string with part of ticker, FIGI or instrument's name.
1267        :param show: if `True` then print results to console, if `False` — return list of result only.
1268        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1269        :return: list of dictionaries with all found instruments.
1270        """
1271        if not self.iList:
1272            self.iList = self.Listing()
1273
1274        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1275        compiledPattern = re.compile(pattern, re.IGNORECASE)
1276
1277        for iType in self.iList:
1278            for instrument in self.iList[iType].values():
1279                searchResult = compiledPattern.search(" ".join(
1280                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1281                ))
1282
1283                if searchResult:
1284                    searchResults[iType][instrument["ticker"]] = instrument
1285
1286        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1287        info = [
1288            "# Search results\n\n",
1289            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1290            "* **Search pattern:** [{}]\n".format(pattern),
1291            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1292            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1293        ]
1294        infoShort = info[:]
1295
1296        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1297        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1298        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1299
1300        if resultsLen == 0:
1301            info.append("\nNo results\n")
1302            infoShort.append("\nNo results\n")
1303            uLogger.warning("No results. Try changing your search pattern.")
1304
1305        else:
1306            for iType in searchResults:
1307                iTypeValuesCount = len(searchResults[iType].values())
1308                if iTypeValuesCount > 0:
1309                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1310                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1311
1312                    for instrument in searchResults[iType].values():
1313                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1314                            instrument["type"],
1315                            instrument["ticker"],
1316                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1317                            instrument["figi"],
1318                        ))
1319
1320                    if iTypeValuesCount <= 5:
1321                        infoShort.extend(info[-iTypeValuesCount:])
1322
1323                    else:
1324                        infoShort.extend(info[-5:])
1325                        infoShort.append(skippedLine)
1326
1327        infoText = "".join(info)
1328        infoTextShort = "".join(infoShort)
1329
1330        if show and not onlyFiles:
1331            uLogger.info(infoTextShort)
1332            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1333
1334        if self.searchResultsFile and (show or onlyFiles):
1335            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1336                fH.write(infoText)
1337
1338            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1339
1340            if self.useHTMLReports:
1341                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1342                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1343                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1344
1345                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1346
1347        return searchResults
1348
1349    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1350        """
1351        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1352
1353        :param instruments: list of strings with tickers or FIGIs.
1354        :return: list with unique instrument FIGIs only.
1355        """
1356        requestedInstruments = []
1357        for iName in instruments:
1358            if iName not in self.aliases.keys():
1359                if iName not in requestedInstruments:
1360                    requestedInstruments.append(iName)
1361
1362            else:
1363                if iName not in requestedInstruments:
1364                    if self.aliases[iName] not in requestedInstruments:
1365                        requestedInstruments.append(self.aliases[iName])
1366
1367        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1368
1369        onlyUniqueFIGIs = []
1370        for iName in requestedInstruments:
1371            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1372                continue
1373
1374            self._ticker = iName
1375            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1376
1377            if not iData:
1378                self._ticker = ""
1379                self._figi = iName
1380
1381                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1382
1383                if not iData:
1384                    self._figi = ""
1385                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1386
1387            if iData and iData["figi"] not in onlyUniqueFIGIs:
1388                onlyUniqueFIGIs.append(iData["figi"])
1389
1390        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1391
1392        return onlyUniqueFIGIs
1393
1394    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1395        """
1396        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1397
1398        See limits: https://tinkoff.github.io/investAPI/limits/
1399
1400        If `pricesFile` string is not empty then also save information to this file.
1401
1402        :param instruments: list of strings with tickers or FIGIs.
1403        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1404        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1405        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1406                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1407        """
1408        if instruments is None or not instruments:
1409            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1410            raise Exception("Ticker or FIGI required")
1411
1412        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1413
1414        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1415
1416        iList = []  # trying to get info and current prices about all unique instruments:
1417        for self._figi in onlyUniqueFIGIs:
1418            iData = self.SearchByFIGI(requestPrice=True, show=False)
1419            iList.append(iData)
1420
1421        self.ShowListOfPrices(iList, show, onlyFiles)
1422
1423        return iList
1424
1425    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1426        """
1427        Show table contains current prices of given instruments.
1428
1429        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1430                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1431        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1432        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1433        :return: multilines text in Markdown format as a table contains current prices.
1434        """
1435        infoText = ""
1436
1437        if show or self.pricesFile or onlyFiles:
1438            info = [
1439                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1440                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1441                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1442            ]
1443
1444            for item in iList:
1445                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1446                    item["ticker"],
1447                    item["figi"],
1448                    item["type"],
1449                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1450                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1451                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1452                    "{} / {}".format(
1453                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1454                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1455                    ),
1456                    "{} / {}".format(
1457                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1458                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1459                    ),
1460                    item["currency"],
1461                ))
1462
1463            infoText = "".join(info)
1464
1465            if show and not onlyFiles:
1466                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1467
1468            if self.pricesFile and (show or onlyFiles):
1469                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1470                    fH.write(infoText)
1471
1472                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1473
1474                if self.useHTMLReports:
1475                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1476                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1477                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1478
1479                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1480
1481        return infoText
1482
1483    def RequestTradingStatus(self) -> dict:
1484        """
1485        Requesting trading status for the instrument defined by `figi` variable.
1486
1487        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1488
1489        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1490
1491        :return: dictionary with trading status attributes. Response example:
1492                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1493                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1494        """
1495        if self._figi is None or not self._figi:
1496            uLogger.error("Variable `figi` must be defined for using this method!")
1497            raise Exception("FIGI required")
1498
1499        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1500
1501        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1502        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1503        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1504
1505        if self.moreDebug:
1506            uLogger.debug("Records about current trading status successfully received")
1507
1508        return tradingStatus
1509
1510    def RequestPortfolio(self) -> dict:
1511        """
1512        Requesting actual user's portfolio for current `accountId`.
1513
1514        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1515
1516        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1517
1518        :return: dictionary with user's portfolio.
1519        """
1520        if self.accountId is None or not self.accountId:
1521            uLogger.error("Variable `accountId` must be defined for using this method!")
1522            raise Exception("Account ID required")
1523
1524        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1525
1526        self.body = str({"accountId": self.accountId})
1527        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1528        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1529
1530        if self.moreDebug:
1531            uLogger.debug("Records about user's portfolio successfully received")
1532
1533        return rawPortfolio
1534
1535    def RequestPositions(self) -> dict:
1536        """
1537        Requesting open positions by currencies and instruments for current `accountId`.
1538
1539        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1540
1541        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1542
1543        :return: dictionary with open positions by instruments.
1544        """
1545        if self.accountId is None or not self.accountId:
1546            uLogger.error("Variable `accountId` must be defined for using this method!")
1547            raise Exception("Account ID required")
1548
1549        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1550
1551        self.body = str({"accountId": self.accountId})
1552        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1553        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1554
1555        if self.moreDebug:
1556            uLogger.debug("Records about current open positions successfully received")
1557
1558        return rawPositions
1559
1560    def RequestPendingOrders(self) -> list:
1561        """
1562        Requesting current actual pending limit orders for current `accountId`.
1563
1564        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1565
1566        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1567
1568        :return: list of dictionaries with pending limit orders.
1569        """
1570        if self.accountId is None or not self.accountId:
1571            uLogger.error("Variable `accountId` must be defined for using this method!")
1572            raise Exception("Account ID required")
1573
1574        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1575
1576        self.body = str({"accountId": self.accountId})
1577        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1578        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1579
1580        if "orders" in rawResponse.keys():
1581            rawOrders = rawResponse["orders"]
1582            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1583
1584        else:
1585            rawOrders = []
1586            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1587
1588        return rawOrders
1589
1590    def RequestStopOrders(self) -> list:
1591        """
1592        Requesting current actual stop orders for current `accountId`.
1593
1594        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1595
1596        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1597
1598        :return: list of dictionaries with stop orders.
1599        """
1600        if self.accountId is None or not self.accountId:
1601            uLogger.error("Variable `accountId` must be defined for using this method!")
1602            raise Exception("Account ID required")
1603
1604        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1605
1606        self.body = str({"accountId": self.accountId})
1607        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1608        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1609
1610        if "stopOrders" in rawResponse.keys():
1611            rawStopOrders = rawResponse["stopOrders"]
1612            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1613
1614        else:
1615            rawStopOrders = []
1616            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1617
1618        return rawStopOrders
1619
1620    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1621        """
1622        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1623        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1624        and `overviewBondsCalendarFile` are defined then also save information to file.
1625
1626        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1627        many requests about the state of the portfolio, and then, based on the received data, a large number
1628        of calculation and statistics are collected.
1629
1630        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1631        :param details: how detailed should the information be?
1632        - `full` — shows full available information about portfolio status (by default),
1633        - `positions` — shows only open positions,
1634        - `orders` — shows only sections of open limits and stop orders.
1635        - `digest` — show a short digest of the portfolio status,
1636        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1637        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1638        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1639        :return: dictionary with client's raw portfolio and some statistics.
1640        """
1641        if self.accountId is None or not self.accountId:
1642            uLogger.error("Variable `accountId` must be defined for using this method!")
1643            raise Exception("Account ID required")
1644
1645        view = {
1646            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1647                "headers": {},  # list of dictionaries, response headers without "positions" section
1648                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1649                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1650                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1651                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1652                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1653                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1654                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1655                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1656                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1657            },
1658            "stat": {  # --- some statistics calculated using "raw" sections:
1659                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1660                "availableRUB": 0.,  # available rubles (without other currencies)
1661                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1662                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1663                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1664                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1665                "sharesCostRUB": 0.,  # costs of all shares in RUB
1666                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1667                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1668                "futuresCostRUB": 0.,  # costs of all futures in RUB
1669                "Currencies": [],  # list of dictionaries of all currencies statistics
1670                "Shares": [],  # list of dictionaries of all shares statistics
1671                "Bonds": [],  # list of dictionaries of all bonds statistics
1672                "Etfs": [],  # list of dictionaries of all etfs statistics
1673                "Futures": [],  # list of dictionaries of all futures statistics
1674                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1675                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1676                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1677                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1678                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1679            },
1680            "analytics": {  # --- some analytics of portfolio:
1681                "distrByAssets": {},  # portfolio distribution by assets
1682                "distrByCompanies": {},  # portfolio distribution by companies
1683                "distrBySectors": {},  # portfolio distribution by sectors
1684                "distrByCurrencies": {},  # portfolio distribution by currencies
1685                "distrByCountries": {},  # portfolio distribution by countries
1686                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1687            }
1688        }
1689
1690        details = details.lower()
1691        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1692        if details not in availableDetails:
1693            details = "full"
1694            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1695
1696        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1697
1698        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1699        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1700        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1701        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1702
1703        # save response headers without "positions" section:
1704        for key in portfolioResponse.keys():
1705            if key != "positions":
1706                view["raw"]["headers"][key] = portfolioResponse[key]
1707
1708            else:
1709                continue
1710
1711        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1712        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1713        for item in portfolioResponse["positions"]:
1714            if item["instrumentType"] == "currency":
1715                self._figi = item["figi"]
1716                if not self._figi and item["ticker"]:
1717                    self._ticker = item["ticker"]
1718                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1719
1720                curr = self.SearchByFIGI(requestPrice=False)
1721
1722                # current price of currency in RUB:
1723                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1724                    "name": curr["name"],
1725                    "currentPrice": NanoToFloat(
1726                        item["currentPrice"]["units"],
1727                        item["currentPrice"]["nano"]
1728                    ),
1729                }
1730
1731                view["raw"]["Currencies"].append(item)
1732
1733            elif item["instrumentType"] == "share":
1734                view["raw"]["Shares"].append(item)
1735
1736            elif item["instrumentType"] == "bond":
1737                view["raw"]["Bonds"].append(item)
1738
1739            elif item["instrumentType"] == "etf":
1740                view["raw"]["Etfs"].append(item)
1741
1742            elif item["instrumentType"] == "futures":
1743                view["raw"]["Futures"].append(item)
1744
1745            else:
1746                continue
1747
1748        # how many volume of currencies (by ISO currency name) are blocked:
1749        for item in view["raw"]["positions"]["blocked"]:
1750            blocked = NanoToFloat(item["units"], item["nano"])
1751            if blocked > 0:
1752                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1753
1754        # how many volume of instruments (by FIGI) are blocked:
1755        for item in view["raw"]["positions"]["securities"]:
1756            blocked = int(item["blocked"])
1757            if blocked > 0:
1758                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1759
1760        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1761
1762        if "rub" in allBlocked.keys():
1763            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1764
1765        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1766        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1767        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1768        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1769        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1770        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1771        view["stat"]["portfolioCostRUB"] = sum([
1772            view["stat"]["allCurrenciesCostRUB"],
1773            view["stat"]["sharesCostRUB"],
1774            view["stat"]["bondsCostRUB"],
1775            view["stat"]["etfsCostRUB"],
1776            view["stat"]["futuresCostRUB"],
1777        ])
1778
1779        # --- calculating some portfolio statistics:
1780        byComp = {}  # distribution by companies
1781        bySect = {}  # distribution by sectors
1782        byCurr = {}  # distribution by currencies (include RUB)
1783        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1784        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1785
1786        for item in portfolioResponse["positions"]:
1787            self._figi = item["figi"]
1788            if not self._figi and item["ticker"]:
1789                self._ticker = item["ticker"]
1790                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1791
1792            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1793
1794            if instrument:
1795                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1796                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1797
1798                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1799                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1800
1801                else:
1802                    blocked = 0
1803
1804                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1805                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1806                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1807                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1808                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1809                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1810                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1811                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1812                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1813                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1814                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1815                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1816
1817                statData = {
1818                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1819                    "ticker": instrument["ticker"],  # ticker by FIGI
1820                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1821                    "volume": volume,  # available volume of instrument
1822                    "lots": lots,  # volume in lots of instrument
1823                    "direction": direction,  # direction of an instrument's position: short or long
1824                    "blocked": blocked,  # blocked volume of currency or instrument
1825                    "currentPrice": curPrice,  # current instrument's price in basic asset
1826                    "average": average,  # current average position price
1827                    "cost": cost,  # current cost of all volume of instrument in basic asset
1828                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1829                    "costRUB": costRUB,  # cost of instrument in ruble
1830                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1831                    "profit": profit,  # expected profit at current moment
1832                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1833                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1834                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1835                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1836                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1837                    "step": instrument["step"],  # minimum price increment
1838                }
1839
1840                # adding distribution by unique countries:
1841                if statData["country"] not in byCountry.keys():
1842                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1843
1844                else:
1845                    byCountry[statData["country"]]["cost"] += costRUB
1846                    byCountry[statData["country"]]["percent"] += percentCostRUB
1847
1848                if item["instrumentType"] != "currency":
1849                    # adding distribution by unique companies:
1850                    if statData["name"]:
1851                        if statData["name"] not in byComp.keys():
1852                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1853
1854                        else:
1855                            byComp[statData["name"]]["cost"] += costRUB
1856                            byComp[statData["name"]]["percent"] += percentCostRUB
1857
1858                    # adding distribution by unique sectors:
1859                    if statData["sector"] not in bySect.keys():
1860                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1861
1862                    else:
1863                        bySect[statData["sector"]]["cost"] += costRUB
1864                        bySect[statData["sector"]]["percent"] += percentCostRUB
1865
1866                # adding distribution by unique currencies:
1867                if currency not in byCurr.keys():
1868                    byCurr[currency] = {
1869                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1870                        "cost": costRUB,
1871                        "percent": percentCostRUB
1872                    }
1873
1874                else:
1875                    byCurr[currency]["cost"] += costRUB
1876                    byCurr[currency]["percent"] += percentCostRUB
1877
1878                # saving statistics for every instrument:
1879                if item["instrumentType"] == "currency":
1880                    view["stat"]["Currencies"].append(statData)
1881
1882                    # update dict with free funds for trading (total - blocked) by currencies
1883                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1884                    view["stat"]["funds"][currency] = {
1885                        "total": volume,
1886                        "totalCostRUB": costRUB,  # total volume cost in rubles
1887                        "free": volume - blocked,
1888                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1889                    }
1890
1891                elif item["instrumentType"] == "share":
1892                    view["stat"]["Shares"].append(statData)
1893
1894                elif item["instrumentType"] == "bond":
1895                    view["stat"]["Bonds"].append(statData)
1896
1897                elif item["instrumentType"] == "etf":
1898                    view["stat"]["Etfs"].append(statData)
1899
1900                elif item["instrumentType"] == "Futures":
1901                    view["stat"]["Futures"].append(statData)
1902
1903                else:
1904                    continue
1905
1906        # total changes in Russian Ruble:
1907        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1908        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1909        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1910        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1911        view["stat"]["funds"]["rub"] = {
1912            "total": view["stat"]["availableRUB"],
1913            "totalCostRUB": view["stat"]["availableRUB"],
1914            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1915            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1916        }
1917
1918        # --- pending limit orders sector data:
1919        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1920        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1921
1922        for item in view["raw"]["orders"]:
1923            self._figi = item["figi"]
1924
1925            if item["figi"] not in uniquePendingOrdersFIGIs:
1926                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1927
1928                uniquePendingOrdersFIGIs.append(item["figi"])
1929                uniquePendingOrders[item["figi"]] = instrument
1930
1931            else:
1932                instrument = uniquePendingOrders[item["figi"]]
1933
1934            if instrument:
1935                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1936                orderType = TKS_ORDER_TYPES[item["orderType"]]
1937                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1938                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1939
1940                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1941                if item["direction"] == "ORDER_DIRECTION_BUY":
1942                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1943
1944                else:
1945                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1946
1947                # requested price for order execution:
1948                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1949
1950                # necessary changes in percent to reach target from current price:
1951                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1952
1953                view["stat"]["orders"].append({
1954                    "orderID": item["orderId"],  # orderId number parameter of current order
1955                    "figi": item["figi"],  # FIGI identification
1956                    "ticker": instrument["ticker"],  # ticker name by FIGI
1957                    "lotsRequested": item["lotsRequested"],  # requested lots value
1958                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1959                    "currentPrice": lastPrice,  # current instrument's price for defined action
1960                    "targetPrice": target,  # requested price for order execution in base currency
1961                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1962                    "percentChanges": changes,  # changes in percent to target from current price
1963                    "currency": item["currency"],  # instrument's currency name
1964                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1965                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1966                    "status": orderState,  # order status from TKS_ORDER_STATES
1967                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1968                })
1969
1970        # --- stop orders sector data:
1971        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1972        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1973
1974        for item in view["raw"]["stopOrders"]:
1975            self._figi = item["figi"]
1976
1977            if item["figi"] not in uniqueStopOrdersFIGIs:
1978                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1979
1980                uniqueStopOrdersFIGIs.append(item["figi"])
1981                uniqueStopOrders[item["figi"]] = instrument
1982
1983            else:
1984                instrument = uniqueStopOrders[item["figi"]]
1985
1986            if instrument:
1987                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1988                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1989                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1990
1991                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1992                if "expirationTime" in item.keys():
1993                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1994                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1995
1996                else:
1997                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1998                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1999
2000                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2001                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2002                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2003
2004                else:
2005                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2006
2007                # requested price when stop-order executed:
2008                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2009
2010                # price for limit-order, set up when stop-order executed:
2011                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2012
2013                # necessary changes in percent to reach target from current price:
2014                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2015
2016                view["stat"]["stopOrders"].append({
2017                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2018                    "figi": item["figi"],  # FIGI identification
2019                    "ticker": instrument["ticker"],  # ticker name by FIGI
2020                    "lotsRequested": item["lotsRequested"],  # requested lots value
2021                    "currentPrice": lastPrice,  # current instrument's price for defined action
2022                    "targetPrice": target,  # requested price for stop-order execution in base currency
2023                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2024                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2025                    "percentChanges": changes,  # changes in percent to target from current price
2026                    "currency": item["currency"],  # instrument's currency name
2027                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2028                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2029                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2030                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2031                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2032                })
2033
2034        # --- calculating data for analytics section:
2035        # portfolio distribution by assets:
2036        view["analytics"]["distrByAssets"] = {
2037            "Ruble": {
2038                "uniques": 1,
2039                "cost": view["stat"]["availableRUB"],
2040                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2041            },
2042            "Currencies": {
2043                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2044                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2045                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2046            },
2047            "Shares": {
2048                "uniques": len(view["stat"]["Shares"]),
2049                "cost": view["stat"]["sharesCostRUB"],
2050                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2051            },
2052            "Bonds": {
2053                "uniques": len(view["stat"]["Bonds"]),
2054                "cost": view["stat"]["bondsCostRUB"],
2055                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2056            },
2057            "Etfs": {
2058                "uniques": len(view["stat"]["Etfs"]),
2059                "cost": view["stat"]["etfsCostRUB"],
2060                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2061            },
2062            "Futures": {
2063                "uniques": len(view["stat"]["Futures"]),
2064                "cost": view["stat"]["futuresCostRUB"],
2065                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2066            },
2067        }
2068
2069        # portfolio distribution by companies:
2070        view["analytics"]["distrByCompanies"]["All money cash"] = {
2071            "ticker": "",
2072            "cost": view["stat"]["allCurrenciesCostRUB"],
2073            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2074        }
2075        view["analytics"]["distrByCompanies"].update(byComp)
2076
2077        # portfolio distribution by sectors:
2078        view["analytics"]["distrBySectors"]["All money cash"] = {
2079            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2080            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2081        }
2082        view["analytics"]["distrBySectors"].update(bySect)
2083
2084        # portfolio distribution by currencies:
2085        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2086            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2087
2088            if self.moreDebug:
2089                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2090
2091        view["analytics"]["distrByCurrencies"].update(byCurr)
2092        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2093        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2094
2095        # portfolio distribution by countries:
2096        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2097            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2098
2099            if self.moreDebug:
2100                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2101
2102        view["analytics"]["distrByCountries"].update(byCountry)
2103        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2104        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2105
2106        # --- Prepare text statistics overview in human-readable:
2107        if show or onlyFiles:
2108            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2109
2110            # Whatever the value `details`, header not changes:
2111            info = [
2112                "# Client's portfolio\n\n",
2113                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2114                "* **Account ID:** [{}]\n".format(self.accountId),
2115            ]
2116
2117            if details in ["full", "positions", "digest"]:
2118                info.extend([
2119                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2120                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2121                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2122                        view["stat"]["totalChangesRUB"],
2123                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2124                        view["stat"]["totalChangesPercentRUB"],
2125                    ),
2126                ])
2127
2128            if details in ["full", "positions"]:
2129                info.extend([
2130                    "## Open positions\n\n",
2131                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2132                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2133                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2134                        "{:.2f} ({:.2f}) rub".format(
2135                            view["stat"]["availableRUB"],
2136                            view["stat"]["blockedRUB"],
2137                        )
2138                    )
2139                ])
2140
2141                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2142                    return [
2143                        "|                             |                                 |          |              |              |                     |                              |\n",
2144                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2145                            noTradeStr if noTradeStr else typeStr,
2146                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2147                        ),
2148                    ]
2149
2150                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2151                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2152                        "{} [{}]".format(data["ticker"], data["figi"]),
2153                        "{:.2f} ({:.2f}) {}".format(
2154                            data["volume"],
2155                            data["blocked"],
2156                            data["currency"],
2157                        ) if isCurr else "{:.0f} ({:.0f})".format(
2158                            data["volume"],
2159                            data["blocked"],
2160                        ),
2161                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2162                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2163                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2164                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2165                        "{}{:.2f} {} ({}{:.2f}%)".format(
2166                            "+" if data["profit"] > 0 else "",
2167                            data["profit"], data["baseCurrencyName"],
2168                            "+" if data["percentProfit"] > 0 else "",
2169                            data["percentProfit"],
2170                        ),
2171                    )
2172
2173                # --- Show currencies section:
2174                if view["stat"]["Currencies"]:
2175                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2176                    for item in view["stat"]["Currencies"]:
2177                        info.append(_InfoStr(item, isCurr=True))
2178
2179                else:
2180                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2181
2182                # --- Show shares section:
2183                if view["stat"]["Shares"]:
2184                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2185
2186                    for item in view["stat"]["Shares"]:
2187                        info.append(_InfoStr(item))
2188
2189                else:
2190                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2191
2192                # --- Show bonds section:
2193                if view["stat"]["Bonds"]:
2194                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2195
2196                    for item in view["stat"]["Bonds"]:
2197                        info.append(_InfoStr(item))
2198
2199                else:
2200                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2201
2202                # --- Show etfs section:
2203                if view["stat"]["Etfs"]:
2204                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2205
2206                    for item in view["stat"]["Etfs"]:
2207                        info.append(_InfoStr(item))
2208
2209                else:
2210                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2211
2212                # --- Show futures section:
2213                if view["stat"]["Futures"]:
2214                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2215
2216                    for item in view["stat"]["Futures"]:
2217                        info.append(_InfoStr(item))
2218
2219                else:
2220                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2221
2222            if details in ["full", "orders"]:
2223                # --- Show pending limit orders section:
2224                if view["stat"]["orders"]:
2225                    info.extend([
2226                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2227                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2228                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2229                    ])
2230
2231                    for item in view["stat"]["orders"]:
2232                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2233                            "{} [{}]".format(item["ticker"], item["figi"]),
2234                            item["orderID"],
2235                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2236                            "{} {} ({}{:.2f}%)".format(
2237                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2238                                item["baseCurrencyName"],
2239                                "+" if item["percentChanges"] > 0 else "",
2240                                float(item["percentChanges"]),
2241                            ),
2242                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2243                            item["action"],
2244                            item["type"],
2245                            item["date"],
2246                        ))
2247
2248                else:
2249                    info.append("\n## Total pending limit-orders: [0]\n")
2250
2251                # --- Show stop orders section:
2252                if view["stat"]["stopOrders"]:
2253                    info.extend([
2254                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2255                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2256                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2257                    ])
2258
2259                    for item in view["stat"]["stopOrders"]:
2260                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2261                            "{} [{}]".format(item["ticker"], item["figi"]),
2262                            item["orderID"],
2263                            item["lotsRequested"],
2264                            "{} {} ({}{:.2f}%)".format(
2265                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2266                                item["baseCurrencyName"],
2267                                "+" if item["percentChanges"] > 0 else "",
2268                                float(item["percentChanges"]),
2269                            ),
2270                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2271                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2272                            item["action"],
2273                            item["type"],
2274                            item["expType"],
2275                            item["createDate"],
2276                            item["expDate"],
2277                        ))
2278
2279                else:
2280                    info.append("\n## Total stop-orders: [0]\n")
2281
2282            if details in ["full", "analytics"]:
2283                # -- Show analytics section:
2284                if view["stat"]["portfolioCostRUB"] > 0:
2285                    info.extend([
2286                        "\n# Analytics\n\n"
2287                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2288                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2289                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2290                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2291                            view["stat"]["totalChangesRUB"],
2292                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2293                            view["stat"]["totalChangesPercentRUB"],
2294                        ),
2295                        "\n## Portfolio distribution by assets\n"
2296                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2297                        "|------------------------------------|---------|---------|--------------------|\n",
2298                    ])
2299
2300                    for key in view["analytics"]["distrByAssets"].keys():
2301                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2302                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2303                                key,
2304                                view["analytics"]["distrByAssets"][key]["uniques"],
2305                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2306                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2307                            ))
2308
2309                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2310
2311                    info.extend([
2312                        "\n## Portfolio distribution by companies\n"
2313                        "\n| Company                                      | Percent | Current cost       |\n",
2314                        aSepLine,
2315                    ])
2316
2317                    for company in view["analytics"]["distrByCompanies"].keys():
2318                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2319                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2320                                "{}{}".format(
2321                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2322                                    company,
2323                                ),
2324                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2325                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2326                            ))
2327
2328                    info.extend([
2329                        "\n## Portfolio distribution by sectors\n"
2330                        "\n| Sector                                       | Percent | Current cost       |\n",
2331                        aSepLine,
2332                    ])
2333
2334                    for sector in view["analytics"]["distrBySectors"].keys():
2335                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2336                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2337                                sector,
2338                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2339                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2340                            ))
2341
2342                    info.extend([
2343                        "\n## Portfolio distribution by currencies\n"
2344                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2345                        aSepLine,
2346                    ])
2347
2348                    for curr in view["analytics"]["distrByCurrencies"].keys():
2349                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2350                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2351                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2352                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2353                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2354                            ))
2355
2356                    info.extend([
2357                        "\n## Portfolio distribution by countries\n"
2358                        "\n| Assets by country                            | Percent | Current cost       |\n",
2359                        aSepLine,
2360                    ])
2361
2362                    for country in view["analytics"]["distrByCountries"].keys():
2363                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2364                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2365                                country,
2366                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2367                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2368                            ))
2369
2370            if details in ["full", "calendar"]:
2371                # -- Show bonds payment calendar section:
2372                if view["stat"]["Bonds"]:
2373                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2374                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2375                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2376
2377                else:
2378                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2379
2380            infoText = "".join(info)
2381
2382            if show and not onlyFiles:
2383                uLogger.info(infoText)
2384
2385            if details == "full" and self.overviewFile:
2386                filename = self.overviewFile
2387
2388            elif details == "digest" and self.overviewDigestFile:
2389                filename = self.overviewDigestFile
2390
2391            elif details == "positions" and self.overviewPositionsFile:
2392                filename = self.overviewPositionsFile
2393
2394            elif details == "orders" and self.overviewOrdersFile:
2395                filename = self.overviewOrdersFile
2396
2397            elif details == "analytics" and self.overviewAnalyticsFile:
2398                filename = self.overviewAnalyticsFile
2399
2400            elif details == "calendar" and self.overviewBondsCalendarFile:
2401                filename = self.overviewBondsCalendarFile
2402
2403            else:
2404                filename = ""
2405
2406            if filename and (show or onlyFiles):
2407                with open(filename, "w", encoding="UTF-8") as fH:
2408                    fH.write(infoText)
2409
2410                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2411
2412                if self.useHTMLReports:
2413                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2414                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2415                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2416
2417                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2418
2419        return view
2420
2421    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2422        """
2423        Returns history operations between two given dates for current `accountId`.
2424        If `reportFile` string is not empty then also save human-readable report.
2425        Shows some statistical data of closed positions.
2426
2427        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2428        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2429        :param show: if `True` then also prints all records to the console.
2430        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2431        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2432        :return: original list of dictionaries with history of deals records from API ("operations" key):
2433                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2434                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2435        """
2436        if self.accountId is None or not self.accountId:
2437            uLogger.error("Variable `accountId` must be defined for using this method!")
2438            raise Exception("Account ID required")
2439
2440        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2441
2442        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2443
2444        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2445        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2446        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2447        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2448        customStat = {}  # custom statistics in additional to responseJSON
2449
2450        # --- output report in human-readable format:
2451        if self.reportFile and (show or onlyFiles):
2452            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2453            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2454            nextDay = ""
2455
2456            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2457
2458            if len(ops) > 0:
2459                customStat = {
2460                    "opsCount": 0,  # total operations count
2461                    "buyCount": 0,  # buy operations
2462                    "sellCount": 0,  # sell operations
2463                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2464                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2465                    "payIn": {"rub": 0.},  # Deposit brokerage account
2466                    "payOut": {"rub": 0.},  # Withdrawals
2467                    "divs": {"rub": 0.},  # Dividends income
2468                    "coupons": {"rub": 0.},  # Coupon's income
2469                    "brokerCom": {"rub": 0.},  # Service commissions
2470                    "serviceCom": {"rub": 0.},  # Service commissions
2471                    "marginCom": {"rub": 0.},  # Margin commissions
2472                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2473                }
2474
2475                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2476                for item in ops:
2477                    if item["state"] == "OPERATION_STATE_EXECUTED":
2478                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2479
2480                        # count buy operations:
2481                        if "_BUY" in item["operationType"]:
2482                            customStat["buyCount"] += 1
2483
2484                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2485                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2486
2487                            else:
2488                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2489
2490                        # count sell operations:
2491                        elif "_SELL" in item["operationType"]:
2492                            customStat["sellCount"] += 1
2493
2494                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2495                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2496
2497                            else:
2498                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2499
2500                        # count incoming operations:
2501                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2502                            if item["payment"]["currency"] in customStat["payIn"].keys():
2503                                customStat["payIn"][item["payment"]["currency"]] += payment
2504
2505                            else:
2506                                customStat["payIn"][item["payment"]["currency"]] = payment
2507
2508                        # count withdrawals operations:
2509                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2510                            if item["payment"]["currency"] in customStat["payOut"].keys():
2511                                customStat["payOut"][item["payment"]["currency"]] += payment
2512
2513                            else:
2514                                customStat["payOut"][item["payment"]["currency"]] = payment
2515
2516                        # count dividends income:
2517                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2518                            if item["payment"]["currency"] in customStat["divs"].keys():
2519                                customStat["divs"][item["payment"]["currency"]] += payment
2520
2521                            else:
2522                                customStat["divs"][item["payment"]["currency"]] = payment
2523
2524                        # count coupon's income:
2525                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2526                            if item["payment"]["currency"] in customStat["coupons"].keys():
2527                                customStat["coupons"][item["payment"]["currency"]] += payment
2528
2529                            else:
2530                                customStat["coupons"][item["payment"]["currency"]] = payment
2531
2532                        # count broker commissions:
2533                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2534                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2535                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2536
2537                            else:
2538                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2539
2540                        # count service commissions:
2541                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2542                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2543                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2544
2545                            else:
2546                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2547
2548                        # count margin commissions:
2549                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2550                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2551                                customStat["marginCom"][item["payment"]["currency"]] += payment
2552
2553                            else:
2554                                customStat["marginCom"][item["payment"]["currency"]] = payment
2555
2556                        # count withholding taxes:
2557                        elif "_TAX" in item["operationType"]:
2558                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2559                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2560
2561                            else:
2562                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2563
2564                        else:
2565                            continue
2566
2567                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2568
2569                # --- view "Actions" lines:
2570                info.extend([
2571                    "| Report sections            |                               |                              |                      |                        |\n",
2572                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2573                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2574                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2575                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2576                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2577                    ),
2578                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2579                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2580                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2581                    ),
2582                ])
2583
2584                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2585                for key in opsKeys:
2586                    if key == "rub":
2587                        continue
2588
2589                    info.extend([
2590                        "|                            |                               | {:<28} |                      |                        |\n".format(
2591                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2592                        ),
2593                        "|                            |                               | {:<28} |                      |                        |\n".format(
2594                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2595                        ),
2596                    ])
2597
2598                info.append(splitLine1)
2599
2600                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2601                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2602                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2603                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2604                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2605                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2606                    )
2607
2608                # --- view "Payments" lines:
2609                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2610                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2611
2612                for key in paymentsKeys:
2613                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2614
2615                info.append(splitLine1)
2616
2617                # --- view "Commissions and taxes" lines:
2618                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2619                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2620
2621                for key in comKeys:
2622                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2623
2624                info.extend([
2625                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2626                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2627                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2628                ])
2629
2630            else:
2631                info.append("Broker returned no operations during this period\n")
2632
2633            # --- view "Operations" section:
2634            for item in ops:
2635                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2636                    continue
2637
2638                else:
2639                    self._figi = item["figi"]
2640                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2641                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2642
2643                    # group of deals during one day:
2644                    if nextDay and item["date"].split("T")[0] != nextDay:
2645                        info.append(splitLine2)
2646                        nextDay = ""
2647
2648                    else:
2649                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2650
2651                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2652                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2653                        self._figi if self._figi else "—",
2654                        instrument["ticker"] if instrument else "—",
2655                        instrument["type"] if instrument else "—",
2656                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2657                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2658                        TKS_OPERATION_STATES[item["state"]],
2659                        TKS_OPERATION_TYPES[item["operationType"]],
2660                    ))
2661
2662            infoText = "".join(info)
2663
2664            if show and not onlyFiles:
2665                if self.moreDebug:
2666                    uLogger.debug("Records about history of a client's operations successfully received")
2667
2668                uLogger.info(infoText)
2669
2670            if self.reportFile and (show or onlyFiles):
2671                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2672                    fH.write(infoText)
2673
2674                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2675
2676                if self.useHTMLReports:
2677                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2678                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2679                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2680
2681                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2682
2683        return ops, customStat
2684
2685    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2686        """
2687        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2688
2689        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2690        Warning! Broker server used ISO UTC time by default.
2691
2692        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2693        Also, `historyFile` used to update history with `onlyMissing` parameter.
2694
2695        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2696
2697        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2698        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2699        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2700                         `"hour"`, `"day"`. Default: `"hour"`.
2701        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2702                            False by default. Warning! History appends only from last candle to current time
2703                            with always update last candle!
2704        :param csvSep: separator if csv-file is used, `,` by default.
2705        :param show: if `True` then also prints Pandas DataFrame to the console.
2706        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2707        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2708                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2709        """
2710        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2711        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2712        history = None  # empty pandas object for history
2713
2714        if interval not in TKS_CANDLE_INTERVALS.keys():
2715            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2716            raise Exception("Incorrect value")
2717
2718        if not (self._ticker or self._figi):
2719            uLogger.error("Ticker or FIGI must be defined!")
2720            raise Exception("Ticker or FIGI required")
2721
2722        if self._ticker and not self._figi:
2723            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2724            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2725
2726        if self._figi and not self._ticker:
2727            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2728            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2729
2730        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2731        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2732        if interval.lower() != "day":
2733            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2734
2735        delta = dtEnd - dtStart  # current UTC time minus last time in file
2736        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2737
2738        # calculate history length in candles:
2739        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2740        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2741            length += 1  # to avoid fraction time
2742
2743        # calculate data blocks count:
2744        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2745
2746        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2747        if self.moreDebug:
2748            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2749            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2750            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2751            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2752
2753        tempOld = None  # pandas object for old history, if --only-missing key present
2754        lastTime = None  # datetime object of last old candle in file
2755
2756        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2757            if self.moreDebug:
2758                uLogger.debug("--only-missing key present, add only last missing candles...")
2759                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2760
2761            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2762
2763            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2764            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2765            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2766            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2767
2768            # get last datetime object from last string in file or minus 1 delta if file is empty:
2769            if len(tempOld) > 0:
2770                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2771
2772            else:
2773                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2774
2775            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2776
2777        responseJSONs = []  # raw history blocks of data
2778
2779        blockEnd = dtEnd
2780        for item in range(blocks):
2781            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2782            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2783
2784            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2785                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2786            ))
2787
2788            if blockStart == blockEnd:
2789                uLogger.debug("Skipped this zero-length block...")
2790
2791            else:
2792                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2793                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2794                self.body = str({
2795                    "figi": self._figi,
2796                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2797                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2798                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2799                })
2800                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2801
2802                if "code" in responseJSON.keys():
2803                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2804
2805                else:
2806                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2807                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2808
2809                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2810
2811            blockEnd = blockStart
2812
2813        printCount = len(responseJSONs)  # candles to show in console
2814        if responseJSONs:
2815            tempHistory = pd.DataFrame(
2816                data={
2817                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2818                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2819                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2820                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2821                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2822                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2823                    "volume": [int(item["volume"]) for item in responseJSONs],
2824                },
2825                index=range(len(responseJSONs)),
2826                columns=["date", "time", "open", "high", "low", "close", "volume"],
2827            )
2828            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2829            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2830
2831            # append only newest candles to old history if --only-missing key present:
2832            if onlyMissing and tempOld is not None and lastTime is not None:
2833                index = 0  # find start index in tempHistory data:
2834
2835                for i, item in tempHistory.iterrows():
2836                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2837
2838                    if curTime == lastTime:
2839                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2840                        index = i
2841                        printCount = index + 1
2842                        break
2843
2844                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2845
2846            else:
2847                history = tempHistory  # if no `--only-missing` key then load full data from server
2848
2849            if self.moreDebug:
2850                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2851
2852        if history is not None and not history.empty:
2853            if show and not onlyFiles:
2854                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2855                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2856                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2857                ))
2858
2859        else:
2860            uLogger.warning("Received an empty candles history!")
2861
2862        if self.historyFile is not None:
2863            if history is not None and not history.empty:
2864                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2865                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2866
2867            else:
2868                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2869
2870        else:
2871            if self.moreDebug:
2872                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2873
2874        return history
2875
2876    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2877        """
2878        Load candles history from csv-file and return Pandas DataFrame object.
2879
2880        See also: `History()` and `ShowHistoryChart()` methods.
2881
2882        :param filePath: path to csv-file to open.
2883        """
2884        loadedHistory = None  # init candles data object
2885
2886        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2887
2888        if os.path.exists(filePath):
2889            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2890
2891            tfStr = self.priceModel.FormattedDelta(
2892                self.priceModel.timeframe,
2893                "{days} days {hours}h {minutes}m {seconds}s",
2894            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2895                self.priceModel.timeframe,
2896                "{hours}h {minutes}m {seconds}s",
2897            )
2898
2899            if loadedHistory is not None and not loadedHistory.empty:
2900                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2901                    len(loadedHistory),
2902                    tfStr,
2903                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2904                )
2905
2906            else:
2907                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2908
2909        else:
2910            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2911
2912        return loadedHistory
2913
2914    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2915        """
2916        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2917
2918        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2919        Default: `index.html` (both for interact and non-interact candlesticks chart).
2920
2921        See also: `History()` and `LoadHistory()` methods.
2922
2923        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2924        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2925                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2926                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2927                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2928        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2929                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2930        """
2931        if isinstance(candles, str):
2932            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2933            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2934
2935        elif isinstance(candles, pd.DataFrame):
2936            self.priceModel.prices = candles  # set candles chain from variable
2937            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2938
2939            if "datetime" not in candles.columns:
2940                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2941
2942        else:
2943            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2944            raise Exception("Incorrect value")
2945
2946        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2947
2948        if interact:
2949            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2950
2951            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2952
2953        else:
2954            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2955
2956            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2957
2958        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2959
2960    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2961        """
2962        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2963        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2964
2965        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2966
2967        :param operation: string "Buy" or "Sell".
2968        :param lots: volume, integer count of lots >= 1.
2969        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2970        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2971        :param expDate: string "Undefined" by default or local date in future,
2972                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2973        :return: JSON with response from broker server.
2974        """
2975        if self.accountId is None or not self.accountId:
2976            uLogger.error("Variable `accountId` must be defined for using this method!")
2977            raise Exception("Account ID required")
2978
2979        if operation is None or not operation or operation not in ("Buy", "Sell"):
2980            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2981            raise Exception("Incorrect value")
2982
2983        if lots is None or lots < 1:
2984            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2985            lots = 1
2986
2987        if tp is None or tp < 0:
2988            tp = 0
2989
2990        if sl is None or sl < 0:
2991            sl = 0
2992
2993        if expDate is None or not expDate:
2994            expDate = "Undefined"
2995
2996        if not (self._ticker or self._figi):
2997            uLogger.error("Ticker or FIGI must be defined!")
2998            raise Exception("Ticker or FIGI required")
2999
3000        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3001        self._ticker = instrument["ticker"]
3002        self._figi = instrument["figi"]
3003
3004        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3005
3006        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3007        self.body = str({
3008            "figi": self._figi,
3009            "quantity": str(lots),
3010            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3011            "accountId": str(self.accountId),
3012            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3013        })
3014        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3015
3016        if "orderId" in response.keys():
3017            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3018                operation, response["orderId"],
3019                self._ticker, self._figi, lots,
3020                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3021                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3022                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3023            ))
3024
3025            if tp > 0:
3026                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3027
3028            if sl > 0:
3029                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3030
3031        else:
3032            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3033
3034        return response
3035
3036    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3037        """
3038        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3039        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3040
3041        See also: `Order()` and `Trade()` docstrings.
3042
3043        :param lots: volume, integer count of lots >= 1.
3044        :param tp: float > 0, take profit price of stop-order.
3045        :param sl: float > 0, stop loss price of stop-order.
3046        :param expDate: it's a local date in future.
3047                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3048        :return: JSON with response from broker server.
3049        """
3050        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3051
3052    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3053        """
3054        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3055        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3056
3057        See also: `Order()` and `Trade()` docstrings.
3058
3059        :param lots: volume, integer count of lots >= 1.
3060        :param tp: float > 0, take profit price of stop-order.
3061        :param sl: float > 0, stop loss price of stop-order.
3062        :param expDate: it's a local date in the future.
3063                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3064        :return: JSON with response from broker server.
3065        """
3066        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3067
3068    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3069        """
3070        Close position of given instruments.
3071
3072        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3073        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3074                         This avoids unnecessary downloading data from the server.
3075        """
3076        if instruments is None or not instruments:
3077            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3078            raise Exception("Ticker or FIGI required")
3079
3080        if isinstance(instruments, str):
3081            instruments = [instruments]
3082
3083        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3084        if uniqueInstruments:
3085            if portfolio is None or not portfolio:
3086                portfolio = self.Overview(show=False)
3087
3088            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3089            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3090
3091            for self._figi in uniqueInstruments:
3092                if self._figi not in allOpened:
3093                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3094                    continue
3095
3096                # search open trade info about instrument by ticker:
3097                instrument = {}
3098                for iType in TKS_INSTRUMENTS:
3099                    if instrument:
3100                        break
3101
3102                    for item in portfolio["stat"][iType]:
3103                        if item["figi"] == self._figi:
3104                            instrument = item
3105                            break
3106
3107                if instrument:
3108                    self._ticker = instrument["ticker"]
3109                    self._figi = instrument["figi"]
3110
3111                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3112                        self._ticker,
3113                        self._figi,
3114                        int(instrument["volume"]),
3115                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3116                    ))
3117
3118                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3119
3120                    if tradeLots > 0:
3121                        if instrument["blocked"] > 0:
3122                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3123                                instrument["blocked"],
3124                                self._ticker,
3125                                tradeLots,
3126                            ))
3127
3128                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3129                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3130
3131                    else:
3132                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3133
3134    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3135        """
3136        Close all positions of given instruments with defined type.
3137
3138        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3139        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3140                         This avoids unnecessary downloading data from the server.
3141        """
3142        if iType not in TKS_INSTRUMENTS:
3143            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3144
3145        else:
3146            if portfolio is None or not portfolio:
3147                portfolio = self.Overview(show=False)
3148
3149            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3150            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3151
3152            if tickers and portfolio:
3153                self.CloseTrades(tickers, portfolio)
3154
3155            else:
3156                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3157
3158    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3159        """
3160        Universal method to create market or limit orders with all available parameters for current `accountId`.
3161        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3162
3163        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3164        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3165
3166        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3167        then broker immediately open market order as you can do simple --buy or --sell operations!
3168
3169        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3170        When current price will go up or down to target price value then broker opens a limit order.
3171        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3172
3173        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3174
3175        :param operation: string "Buy" or "Sell".
3176        :param orderType: string "Limit" or "Stop".
3177        :param lots: volume, integer count of lots >= 1.
3178        :param targetPrice: target price > 0. This is open trade price for limit order.
3179        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3180                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3181        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3182                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3183                         Stop loss order always executed by market price.
3184        :param expDate: string "Undefined" by default or local date in future.
3185                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3186                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3187                        A limit order has no expiration date, it lasts until the end of the trading day.
3188        :return: JSON with response from broker server.
3189        """
3190        if self.accountId is None or not self.accountId:
3191            uLogger.error("Variable `accountId` must be defined for using this method!")
3192            raise Exception("Account ID required")
3193
3194        if operation is None or not operation or operation not in ("Buy", "Sell"):
3195            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3196            raise Exception("Incorrect value")
3197
3198        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3199            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3200            raise Exception("Incorrect value")
3201
3202        if lots is None or lots < 1:
3203            uLogger.error("You must define trade volume > 0: integer count of lots!")
3204            raise Exception("Incorrect value")
3205
3206        if targetPrice is None or targetPrice <= 0:
3207            uLogger.error("Target price for limit-order must be greater than 0!")
3208            raise Exception("Incorrect value")
3209
3210        if limitPrice is None or limitPrice <= 0:
3211            limitPrice = targetPrice
3212
3213        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3214            stopType = "Limit"
3215
3216        if expDate is None or not expDate:
3217            expDate = "Undefined"
3218
3219        if not (self._ticker or self._figi):
3220            uLogger.error("Tocker or FIGI must be defined!")
3221            raise Exception("Ticker or FIGI required")
3222
3223        response = {}
3224        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3225        self._ticker = instrument["ticker"]
3226        self._figi = instrument["figi"]
3227
3228        if orderType == "Limit":
3229            uLogger.debug(
3230                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3231                    self._ticker, self._figi,
3232                    operation, lots, targetPrice, instrument["currency"],
3233                ))
3234
3235            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3236            self.body = str({
3237                "figi": self._figi,
3238                "quantity": str(lots),
3239                "price": FloatToNano(targetPrice),
3240                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3241                "accountId": str(self.accountId),
3242                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3243            })
3244            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3245
3246            if "orderId" in response.keys():
3247                uLogger.info(
3248                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3249                        response["orderId"], self._ticker, self._figi, operation, lots,
3250                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3251                    ))
3252
3253                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3254                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3255                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3256                            targetPrice, instrument["currency"],
3257                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3258                        ))
3259
3260                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3261                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3262                            targetPrice, instrument["currency"],
3263                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3264                        ))
3265
3266            else:
3267                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3268
3269        if orderType == "Stop":
3270            uLogger.debug(
3271                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3272                    self._ticker, self._figi,
3273                    operation, lots,
3274                    targetPrice, instrument["currency"],
3275                    limitPrice, instrument["currency"],
3276                    stopType, expDate,
3277                ))
3278
3279            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3280            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3281            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3282
3283            body = {
3284                "figi": self._figi,
3285                "quantity": str(lots),
3286                "price": FloatToNano(limitPrice),
3287                "stopPrice": FloatToNano(targetPrice),
3288                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3289                "accountId": str(self.accountId),
3290                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3291                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3292            }
3293
3294            if expDateUTC:
3295                body["expireDate"] = expDateUTC
3296
3297            self.body = str(body)
3298            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3299
3300            if "stopOrderId" in response.keys():
3301                uLogger.info(
3302                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3303                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3304                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3305                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3306                        TKS_STOP_ORDER_TYPES[stopOrderType],
3307                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3308                    ))
3309
3310                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3311                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3312                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3313                            targetPrice, instrument["currency"],
3314                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3315                        ))
3316
3317                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3318                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3319                            targetPrice, instrument["currency"],
3320                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3321                        ))
3322
3323            else:
3324                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3325
3326        return response
3327
3328    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3329        """
3330        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3331        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3332        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3333        See also: `Order()` docstring.
3334
3335        :param lots: volume, integer count of lots >= 1.
3336        :param targetPrice: target price > 0. This is open trade price for limit order.
3337        :return: JSON with response from broker server.
3338        """
3339        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3340
3341    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3342        """
3343        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3344        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3345        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3346        target price value then broker opens a limit order. See also: `Order()` docstring.
3347
3348        :param lots: volume, integer count of lots >= 1.
3349        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3350        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3351                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3352        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3353                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3354        :param expDate: string "Undefined" by default or local date in future.
3355                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3356                        This date is converting to UTC format for server.
3357        :return: JSON with response from broker server.
3358        """
3359        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3360
3361    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3362        """
3363        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3364        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3365        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3366        See also: `Order()` docstring.
3367
3368        :param lots: volume, integer count of lots >= 1.
3369        :param targetPrice: target price > 0. This is open trade price for limit order.
3370        :return: JSON with response from broker server.
3371        """
3372        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3373
3374    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3375        """
3376        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3377        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3378        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3379        target price value then broker opens a limit order. See also: `Order()` docstring.
3380
3381        :param lots: volume, integer count of lots >= 1.
3382        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3383        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3384                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3385        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3386                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3387        :param expDate: string "Undefined" by default or local date in future.
3388                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3389                        This date is converting to UTC format for server.
3390        :return: JSON with response from broker server.
3391        """
3392        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3393
3394    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3395        """
3396        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3397
3398        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3399        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3400                             This avoids unnecessary downloading data from the server.
3401        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3402        """
3403        if self.accountId is None or not self.accountId:
3404            uLogger.error("Variable `accountId` must be defined for using this method!")
3405            raise Exception("Account ID required")
3406
3407        if orderIDs:
3408            if allOrdersIDs is None:
3409                rawOrders = self.RequestPendingOrders()
3410                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3411
3412            if allStopOrdersIDs is None:
3413                rawStopOrders = self.RequestStopOrders()
3414                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3415
3416            for orderID in orderIDs:
3417                idInPendingOrders = orderID in allOrdersIDs
3418                idInStopOrders = orderID in allStopOrdersIDs
3419
3420                if not (idInPendingOrders or idInStopOrders):
3421                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3422                    continue
3423
3424                else:
3425                    if idInPendingOrders:
3426                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3427
3428                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3429                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3430                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3431                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3432
3433                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3434                            if self.moreDebug:
3435                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3436
3437                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3438
3439                        else:
3440                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3441
3442                    elif idInStopOrders:
3443                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3444
3445                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3446                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3447                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3448                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3449
3450                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3451                            if self.moreDebug:
3452                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3453
3454                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3455
3456                        else:
3457                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3458
3459                    else:
3460                        continue
3461
3462    def CloseAllOrders(self) -> None:
3463        """
3464        Gets a list of open pending and stop orders and cancel it all.
3465        """
3466        rawOrders = self.RequestPendingOrders()
3467        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3468        lenOrders = len(allOrdersIDs)
3469
3470        rawStopOrders = self.RequestStopOrders()
3471        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3472        lenSOrders = len(allStopOrdersIDs)
3473
3474        if lenOrders > 0 or lenSOrders > 0:
3475            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3476
3477            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3478
3479        else:
3480            uLogger.info("Orders not found, nothing to cancel.")
3481
3482    def CloseAll(self, *args) -> None:
3483        """
3484        Close all available (not blocked) opened trades and orders.
3485
3486        Also, you can select one or more keywords case-insensitive:
3487        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3488
3489        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3490        """
3491        overview = self.Overview(show=False)  # get all open trades info
3492
3493        if len(args) == 0:
3494            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3495            self.CloseAllOrders()  # close all pending and stop orders
3496
3497            for iType in TKS_INSTRUMENTS:
3498                if iType != "Currencies":
3499                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3500
3501        else:
3502            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3503            lowerArgs = [x.lower() for x in args]
3504
3505            if "orders" in lowerArgs:
3506                self.CloseAllOrders()  # close all pending and stop orders
3507
3508            for iType in TKS_INSTRUMENTS:
3509                if iType.lower() in lowerArgs and iType != "Currencies":
3510                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3511
3512    def CloseAllByTicker(self, instrument: str) -> None:
3513        """
3514        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3515
3516        This method searches opened trade and orders of instrument throw all portfolio and then use
3517        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3518
3519        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3520
3521        :param instrument: string with ticker.
3522        """
3523        if instrument is None or not instrument:
3524            uLogger.error("Ticker name must be defined for using this method!")
3525            raise Exception("Ticker required")
3526
3527        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3528
3529        self._ticker = instrument  # try to set instrument as ticker
3530        self._figi = ""
3531
3532        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3533        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3534
3535        if limitAll and self.IsInLimitOrders(portfolio=overview):
3536            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3537            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3538
3539        if stopAll and self.IsInStopOrders(portfolio=overview):
3540            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3541            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3542
3543        if self.IsInPortfolio(portfolio=overview):
3544            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3545            self.CloseTrades(instruments=[instrument], portfolio=overview)
3546
3547    def CloseAllByFIGI(self, instrument: str) -> None:
3548        """
3549        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3550
3551        This method searches opened trade and orders of instrument throw all portfolio and then use
3552        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3553
3554        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3555
3556        :param instrument: string with FIGI id.
3557        """
3558        if instrument is None or not instrument:
3559            uLogger.error("FIGI id must be defined for using this method!")
3560            raise Exception("FIGI required")
3561
3562        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3563
3564        self._ticker = ""
3565        self._figi = instrument  # try to set instrument as FIGI id
3566
3567        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3568        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3569
3570        if limitAll and self.IsInLimitOrders(portfolio=overview):
3571            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3572            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3573
3574        if stopAll and self.IsInStopOrders(portfolio=overview):
3575            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3576            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3577
3578        if self.IsInPortfolio(portfolio=overview):
3579            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3580            self.CloseTrades(instruments=[instrument], portfolio=overview)
3581
3582    @staticmethod
3583    def ParseOrderParameters(operation, **inputParameters):
3584        """
3585        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3586
3587        :param operation: string "Buy" or "Sell".
3588        :param inputParameters: this is dict of strings that looks like this
3589               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3590               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3591               "prices" key: one or more prices to open limit-orders
3592               Counts of values in lots and prices lists must be equals!
3593        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3594        """
3595        # TODO: update order grid work with api v2
3596        pass
3597        # uLogger.debug("Input parameters: {}".format(inputParameters))
3598        #
3599        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3600        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3601        #     raise Exception("Incorrect value")
3602        #
3603        # if "l" in inputParameters.keys():
3604        #     inputParameters["lots"] = inputParameters.pop("l")
3605        #
3606        # if "p" in inputParameters.keys():
3607        #     inputParameters["prices"] = inputParameters.pop("p")
3608        #
3609        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3610        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3611        #     raise Exception("Incorrect value")
3612        #
3613        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3614        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3615        #
3616        # if len(lots) != len(prices):
3617        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3618        #     raise Exception("Incorrect value")
3619        #
3620        # uLogger.debug("Extracted parameters for orders:")
3621        # uLogger.debug("lots = {}".format(lots))
3622        # uLogger.debug("prices = {}".format(prices))
3623        #
3624        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3625        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3626        # uLogger.debug("Order parameters: {}".format(result))
3627        #
3628        # return result
3629
3630    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3631        """
3632        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3633
3634        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3635        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3636        """
3637        result = False
3638        msg = "Instrument not defined!"
3639
3640        if portfolio is None or not portfolio:
3641            portfolio = self.Overview(show=False)
3642
3643        if self._ticker:
3644            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3645            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3646
3647            for iType in TKS_INSTRUMENTS:
3648                for instrument in portfolio["stat"][iType]:
3649                    if instrument["ticker"] == self._ticker:
3650                        result = True
3651                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3652                        break
3653
3654        elif self._figi:
3655            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3656            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3657
3658            for iType in TKS_INSTRUMENTS:
3659                for instrument in portfolio["stat"][iType]:
3660                    if instrument["figi"] == self._figi:
3661                        result = True
3662                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3663                        break
3664
3665        else:
3666            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3667
3668        uLogger.debug(msg)
3669
3670        return result
3671
3672    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3673        """
3674        Returns instrument from the user's portfolio if it presents there.
3675        Instrument must be defined by `ticker` (highly priority) or `figi`.
3676
3677        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3678        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3679        """
3680        result = None
3681        msg = "Instrument not defined!"
3682
3683        if portfolio is None or not portfolio:
3684            portfolio = self.Overview(show=False)
3685
3686        if self._ticker:
3687            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3688            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3689
3690            for iType in TKS_INSTRUMENTS:
3691                for instrument in portfolio["stat"][iType]:
3692                    if instrument["ticker"] == self._ticker:
3693                        result = instrument
3694                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3695                        break
3696
3697        elif self._figi:
3698            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3699            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3700
3701            for iType in TKS_INSTRUMENTS:
3702                for instrument in portfolio["stat"][iType]:
3703                    if instrument["figi"] == self._figi:
3704                        result = instrument
3705                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3706                        break
3707
3708        else:
3709            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3710
3711        uLogger.debug(msg)
3712
3713        return result
3714
3715    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3716        """
3717        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3718
3719        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3720
3721        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3722        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3723        """
3724        result = False
3725        msg = "Instrument not defined!"
3726
3727        if portfolio is None or not portfolio:
3728            portfolio = self.Overview(show=False)
3729
3730        if self._ticker:
3731            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3732            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3733
3734            for instrument in portfolio["stat"]["orders"]:
3735                if instrument["ticker"] == self._ticker:
3736                    result = True
3737                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3738                    break
3739
3740        elif self._figi:
3741            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3742            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3743
3744            for instrument in portfolio["stat"]["orders"]:
3745                if instrument["figi"] == self._figi:
3746                    result = True
3747                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3748                    break
3749
3750        else:
3751            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3752
3753        uLogger.debug(msg)
3754
3755        return result
3756
3757    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3758        """
3759        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3760        Instrument must be defined by `ticker` (highly priority) or `figi`.
3761
3762        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3763
3764        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3765        :return: list with `orderID`s of limit orders.
3766        """
3767        result = []
3768        msg = "Instrument not defined!"
3769
3770        if portfolio is None or not portfolio:
3771            portfolio = self.Overview(show=False)
3772
3773        if self._ticker:
3774            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3775            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3776
3777            for instrument in portfolio["stat"]["orders"]:
3778                if instrument["ticker"] == self._ticker:
3779                    result.append(instrument["orderID"])
3780
3781            if result:
3782                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3783
3784        elif self._figi:
3785            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3786            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3787
3788            for instrument in portfolio["stat"]["orders"]:
3789                if instrument["figi"] == self._figi:
3790                    result.append(instrument["orderID"])
3791
3792            if result:
3793                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3794
3795        else:
3796            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3797
3798        uLogger.debug(msg)
3799
3800        return result
3801
3802    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3803        """
3804        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3805
3806        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3807
3808        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3809        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3810        """
3811        result = False
3812        msg = "Instrument not defined!"
3813
3814        if portfolio is None or not portfolio:
3815            portfolio = self.Overview(show=False)
3816
3817        if self._ticker:
3818            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3819            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3820
3821            for instrument in portfolio["stat"]["stopOrders"]:
3822                if instrument["ticker"] == self._ticker:
3823                    result = True
3824                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3825                    break
3826
3827        elif self._figi:
3828            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3829            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3830
3831            for instrument in portfolio["stat"]["stopOrders"]:
3832                if instrument["figi"] == self._figi:
3833                    result = True
3834                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3835                    break
3836
3837        else:
3838            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3839
3840        uLogger.debug(msg)
3841
3842        return result
3843
3844    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3845        """
3846        Returns list with all `orderID`s of opened stop orders for the instrument.
3847        Instrument must be defined by `ticker` (highly priority) or `figi`.
3848
3849        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3850
3851        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3852        :return: list with `orderID`s of stop orders.
3853        """
3854        result = []
3855        msg = "Instrument not defined!"
3856
3857        if portfolio is None or not portfolio:
3858            portfolio = self.Overview(show=False)
3859
3860        if self._ticker:
3861            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3862            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3863
3864            for instrument in portfolio["stat"]["stopOrders"]:
3865                if instrument["ticker"] == self._ticker:
3866                    result.append(instrument["orderID"])
3867
3868            if result:
3869                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3870
3871        elif self._figi:
3872            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3873            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3874
3875            for instrument in portfolio["stat"]["stopOrders"]:
3876                if instrument["figi"] == self._figi:
3877                    result.append(instrument["orderID"])
3878
3879            if result:
3880                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3881
3882        else:
3883            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3884
3885        uLogger.debug(msg)
3886
3887        return result
3888
3889    def RequestLimits(self) -> dict:
3890        """
3891        Method for obtaining the available funds for withdrawal for current `accountId`.
3892
3893        See also:
3894        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3895        - `OverviewLimits()` method
3896
3897        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3898                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3899                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3900                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3901        """
3902        if self.accountId is None or not self.accountId:
3903            uLogger.error("Variable `accountId` must be defined for using this method!")
3904            raise Exception("Account ID required")
3905
3906        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3907
3908        self.body = str({"accountId": self.accountId})
3909        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3910        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3911
3912        if self.moreDebug:
3913            uLogger.debug("Records about available funds for withdrawal successfully received")
3914
3915        return rawLimits
3916
3917    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3918        """
3919        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3920
3921        See also: `RequestLimits()`.
3922
3923        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3924        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3925        :return: dict with raw parsed data from server and some calculated statistics about it.
3926        """
3927        if self.accountId is None or not self.accountId:
3928            uLogger.error("Variable `accountId` must be defined for using this method!")
3929            raise Exception("Account ID required")
3930
3931        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3932
3933        view = {
3934            "rawLimits": rawLimits,
3935            "limits": {  # parsed data for every currency:
3936                "money": {  # this is an array of portfolio currency positions
3937                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3938                },
3939                "blocked": {  # this is an array of blocked currency
3940                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3941                },
3942                "blockedGuarantee": {  # this is locked money under collateral for futures
3943                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3944                },
3945            },
3946        }
3947
3948        # --- Prepare text table with limits in human-readable format:
3949        if show or onlyFiles:
3950            info = [
3951                "# Withdrawal limits\n\n",
3952                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3953                "* **Account ID:** [{}]\n".format(self.accountId),
3954            ]
3955
3956            if view["limits"]["money"]:
3957                info.extend([
3958                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3959                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3960                ])
3961
3962            else:
3963                info.append("\nNo withdrawal limits\n")
3964
3965            for curr in view["limits"]["money"].keys():
3966                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3967                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3968                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3969
3970                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3971                    "[{}]".format(curr),
3972                    "{:.2f}".format(view["limits"]["money"][curr]),
3973                    "{:.2f}".format(availableMoney),
3974                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3975                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3976                )
3977
3978                if curr == "rub":
3979                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3980
3981                else:
3982                    info.append(infoStr)
3983
3984            infoText = "".join(info)
3985
3986            if show and not onlyFiles:
3987                uLogger.info(infoText)
3988
3989            if self.withdrawalLimitsFile and (show or onlyFiles):
3990                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3991                    fH.write(infoText)
3992
3993                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3994
3995                if self.useHTMLReports:
3996                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3997                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3998                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3999
4000                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4001
4002        return view
4003
4004    def RequestAccounts(self) -> dict:
4005        """
4006        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4007
4008        See also:
4009        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4010        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4011        - `OverviewUserInfo()` method
4012
4013        :return: dict with raw data from server that contains accounts info. Example of dict:
4014                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4015                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4016                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4017                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4018        """
4019        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4020
4021        self.body = str({})
4022        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4023        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4024
4025        if self.moreDebug:
4026            uLogger.debug("Records about available accounts successfully received")
4027
4028        return rawAccounts
4029
4030    def RequestUserInfo(self) -> dict:
4031        """
4032        Method for requesting common user's information.
4033
4034        See also:
4035        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4036        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4037        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4038        - `OverviewUserInfo()` method
4039
4040        :return: dict with raw data from server that contains user's information. Example of dict:
4041                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4042                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4043        """
4044        uLogger.debug("Requesting common user's information. Wait, please...")
4045
4046        self.body = str({})
4047        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4048        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4049
4050        if self.moreDebug:
4051            uLogger.debug("Records about current user successfully received")
4052
4053        return rawUserInfo
4054
4055    def RequestMarginStatus(self, accountId: str = None) -> dict:
4056        """
4057        Method for requesting margin calculation for defined account ID.
4058
4059        See also:
4060        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4061        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4062        - `OverviewUserInfo()` method
4063
4064        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4065        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4066                 Example of responses:
4067                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4068                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4069                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4070                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4071                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4072                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4073        """
4074        if accountId is None or not accountId:
4075            if self.accountId is None or not self.accountId:
4076                uLogger.error("Variable `accountId` must be defined for using this method!")
4077                raise Exception("Account ID required")
4078
4079            else:
4080                accountId = self.accountId  # use `self.accountId` (main ID) by default
4081
4082        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4083
4084        self.body = str({"accountId": accountId})
4085        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4086        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4087
4088        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4089            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4090            rawMargin = {}
4091
4092        else:
4093            if self.moreDebug:
4094                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4095
4096        return rawMargin
4097
4098    def RequestTariffLimits(self) -> dict:
4099        """
4100        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4101
4102        See also:
4103        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4104        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4105        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4106        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4107        - `OverviewUserInfo()` method
4108
4109        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4110                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4111                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4112        """
4113        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4114
4115        self.body = str({})
4116        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4117        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4118
4119        if self.moreDebug:
4120            uLogger.debug("Records with limits of current tariff successfully received")
4121
4122        return rawTariffLimits
4123
4124    def RequestBondCoupons(self, iJSON: dict) -> dict:
4125        """
4126        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4127        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4128        All dates are in UTC timezone.
4129
4130        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4131        Documentation:
4132        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4133        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4134
4135        See also: `ExtendBondsData()`.
4136
4137        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4138                      If raw iJSON is not data of bond then server returns an error [400] with message:
4139                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4140        :return: dictionary with bond payment calendar. Response example
4141                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4142                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4143                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4144                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4145        """
4146        if iJSON["figi"] is None or not iJSON["figi"]:
4147            uLogger.error("FIGI must be defined for using this method!")
4148            raise Exception("FIGI required")
4149
4150        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4151        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4152
4153        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4154            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4155            self._figi,
4156            startDate,
4157            endDate,
4158        ))
4159
4160        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4161        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4162        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4163
4164        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4165            uLogger.warning("Instrument type is not bond!")
4166
4167        else:
4168            if self.moreDebug:
4169                uLogger.debug("Records about bond payment calendar successfully received")
4170
4171        return calendar
4172
4173    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4174        """
4175        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4176        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4177        coupon yields, current yields and some statistics etc.
4178
4179        WARNING! This is too long operation if a lot of bonds requested from broker server.
4180
4181        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4182
4183        :param instruments: list of strings with tickers or FIGIs.
4184        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4185                     for further used by data scientists or stock analytics.
4186        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4187                 In XLSX-file and Pandas DataFrame fields mean:
4188                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4189                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4190        """
4191        if instruments is None or not instruments:
4192            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4193            raise Exception("Ticker or FIGI required")
4194
4195        if isinstance(instruments, str):
4196            instruments = [instruments]
4197
4198        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4199
4200        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4201
4202        iCount = len(uniqueInstruments)
4203        tooLong = iCount >= 20
4204        if tooLong:
4205            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4206
4207        bonds = None
4208        for i, self._figi in enumerate(uniqueInstruments):
4209            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4210
4211            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4212                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4213                rawBond = self.SearchByFIGI(requestPrice=True)
4214
4215                # Widen raw data with UTC current time (iData["actualDateTime"]):
4216                actualDate = datetime.now(tzutc())
4217                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4218
4219                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4220                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4221
4222                # Replace some values with human-readable:
4223                iData["nominalCurrency"] = iData["nominal"]["currency"]
4224                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4225                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4226                iData["aciCurrency"] = iData["aciValue"]["currency"]
4227                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4228                iData["issueSize"] = int(iData["issueSize"])
4229                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4230                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4231                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4232                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4233                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4234                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4235                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4236                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4237                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4238                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4239
4240                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4241                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4242                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4243                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4244                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4245                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4246                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4247                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4248                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4249                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4250                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4251
4252                # Widen raw data with calendar data from `rawCalendar` values:
4253                calendarData = []
4254                if "events" in iData["rawCalendar"].keys():
4255                    for item in iData["rawCalendar"]["events"]:
4256                        calendarData.append({
4257                            "couponDate": item["couponDate"],
4258                            "couponNumber": int(item["couponNumber"]),
4259                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4260                            "payCurrency": item["payOneBond"]["currency"],
4261                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4262                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4263                            "couponStartDate": item["couponStartDate"],
4264                            "couponEndDate": item["couponEndDate"],
4265                            "couponPeriod": item["couponPeriod"],
4266                        })
4267
4268                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4269                    if "maturityDate" not in iData.keys():
4270                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4271
4272                # Widen raw data with Coupon Rate.
4273                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4274                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4275                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4276                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4277
4278                # Widen raw data with Yield to Maturity (YTM) on current date.
4279                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4280                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4281                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4282                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4283                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4284                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4285
4286                iData["calendar"] = calendarData  # adds calendar at the end
4287
4288                # Remove not used data:
4289                iData.pop("uid")
4290                iData.pop("positionUid")
4291                iData.pop("currentPrice")
4292                iData.pop("rawCalendar")
4293
4294                colNames = list(iData.keys())
4295                if bonds is None:
4296                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4297
4298                else:
4299                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4300
4301            else:
4302                uLogger.warning("Instrument is not a bond!")
4303
4304            processed = round(100 * (i + 1) / iCount, 1)
4305            if tooLong and processed % 5 == 0:
4306                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4307
4308            else:
4309                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4310
4311        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4312
4313        # Saving bonds from Pandas DataFrame to XLSX sheet:
4314        if xlsx and self.bondsXLSXFile:
4315            with pd.ExcelWriter(
4316                    path=self.bondsXLSXFile,
4317                    date_format=TKS_DATE_FORMAT,
4318                    datetime_format=TKS_DATE_TIME_FORMAT,
4319                    mode="w",
4320            ) as writer:
4321                bonds.to_excel(
4322                    writer,
4323                    sheet_name="Extended bonds data",
4324                    index=True,
4325                    encoding="UTF-8",
4326                    freeze_panes=(1, 1),
4327                )  # saving as XLSX-file with freeze first row and column as headers
4328
4329            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4330
4331        return bonds
4332
4333    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4334        """
4335        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4336
4337        WARNING! This is too long operation if a lot of bonds requested from broker server.
4338
4339        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4340
4341        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4342                        extended information about bonds: main info, current prices, bond payment calendar,
4343                        coupon yields, current yields and some statistics etc.
4344                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4345        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4346                     for further used by data scientists or stock analytics.
4347        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4348        """
4349        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4350            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4351
4352        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4353
4354        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4355        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4356        calendar = None
4357        for bond in extBonds.iterrows():
4358            for item in bond[1]["calendar"]:
4359                cData = {
4360                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4361                    "couponDate": item["couponDate"],
4362                    "figi": bond[1]["figi"],
4363                    "ticker": bond[1]["ticker"],
4364                    "name": bond[1]["name"],
4365                    "couponNumber": item["couponNumber"],
4366                    "payOneBond": item["payOneBond"],
4367                    "payCurrency": item["payCurrency"],
4368                    "couponType": item["couponType"],
4369                    "couponPeriod": item["couponPeriod"],
4370                    "fixDate": item["fixDate"],
4371                    "couponStartDate": item["couponStartDate"],
4372                    "couponEndDate": item["couponEndDate"],
4373                }
4374
4375                if calendar is None:
4376                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4377
4378                else:
4379                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4380
4381        if calendar is not None:
4382            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4383
4384            # Saving calendar from Pandas DataFrame to XLSX sheet:
4385            if xlsx:
4386                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4387
4388                with pd.ExcelWriter(
4389                        path=xlsxCalendarFile,
4390                        date_format=TKS_DATE_FORMAT,
4391                        datetime_format=TKS_DATE_TIME_FORMAT,
4392                        mode="w",
4393                ) as writer:
4394                    humanReadable = calendar.copy(deep=True)
4395                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4396                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4397                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4398                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4399                    humanReadable.columns = colNames  # human-readable column names
4400
4401                    humanReadable.to_excel(
4402                        writer,
4403                        sheet_name="Bond payments calendar",
4404                        index=False,
4405                        encoding="UTF-8",
4406                        freeze_panes=(1, 2),
4407                    )  # saving as XLSX-file with freeze first row and column as headers
4408
4409                    del humanReadable  # release df in memory
4410
4411                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4412
4413        return calendar
4414
4415    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4416        """
4417        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4418        Also, creates Markdown file with calendar data, `calendar.md` by default.
4419
4420        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4421
4422        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4423                        extended information about bonds: main info, current prices, bond payment calendar,
4424                        coupon yields, current yields and some statistics etc.
4425                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4426        :param show: if `True` then also printing bonds payment calendar to the console,
4427                     otherwise save to file `calendarFile` only. `False` by default.
4428        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4429        :return: multilines text in Markdown format with bonds payment calendar as a table.
4430        """
4431        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4432            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4433
4434        infoText = "# Bond payments calendar\n\n"
4435
4436        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4437
4438        if not (calendar is None or calendar.empty):
4439            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4440
4441            info = [
4442                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4443                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4444                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4445            ]
4446
4447            newMonth = False
4448            notOneBond = calendar["figi"].nunique() > 1
4449            for i, bond in enumerate(calendar.iterrows()):
4450                if newMonth and notOneBond:
4451                    info.append(splitLine)
4452
4453                info.append(
4454                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4455                        "  √" if bond[1]["paid"] else "  —",
4456                        bond[1]["couponDate"].split("T")[0],
4457                        bond[1]["figi"],
4458                        bond[1]["ticker"],
4459                        bond[1]["couponNumber"],
4460                        "{} {}".format(
4461                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4462                            bond[1]["payCurrency"],
4463                        ),
4464                        bond[1]["couponType"],
4465                        bond[1]["couponPeriod"],
4466                        bond[1]["fixDate"].split("T")[0],
4467                    )
4468                )
4469
4470                if i < len(calendar.values) - 1:
4471                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4472                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4473                    newMonth = False if curDate.month == nextDate.month else True
4474
4475                else:
4476                    newMonth = False
4477
4478            infoText += "".join(info)
4479
4480            if show and not onlyFiles:
4481                uLogger.info("{}".format(infoText))
4482
4483            if self.calendarFile is not None and (show or onlyFiles):
4484                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4485                    fH.write(infoText)
4486
4487                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4488
4489                if self.useHTMLReports:
4490                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4491                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4492                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4493
4494                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4495
4496        else:
4497            infoText += "No data\n"
4498
4499        return infoText
4500
4501    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4502        """
4503        Method for parsing and show simple table with all available user accounts.
4504
4505        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4506
4507        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4508        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4509        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4510                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4511                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4512                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4513                                                        "closed": "—", "access": "Full access" }, ...}}`
4514        """
4515        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4516
4517        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4518        accounts = {
4519            item["id"]: {
4520                "type": TKS_ACCOUNT_TYPES[item["type"]],
4521                "name": item["name"],
4522                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4523                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4524                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4525                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4526            } for item in rawAccounts["accounts"]
4527        }
4528
4529        # Raw and parsed data with some fields replaced in "stat" section:
4530        view = {
4531            "rawAccounts": rawAccounts,
4532            "stat": accounts,
4533        }
4534
4535        # --- Prepare simple text table with only accounts data in human-readable format:
4536        if show or onlyFiles:
4537            info = [
4538                "# User accounts\n\n",
4539                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4540                "| Account ID   | Type                      | Status                    | Name                           |\n",
4541                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4542            ]
4543
4544            for account in view["stat"].keys():
4545                info.extend([
4546                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4547                        account,
4548                        view["stat"][account]["type"],
4549                        view["stat"][account]["status"],
4550                        view["stat"][account]["name"],
4551                    )
4552                ])
4553
4554            infoText = "".join(info)
4555
4556            if show and not onlyFiles:
4557                uLogger.info(infoText)
4558
4559            if self.userAccountsFile and (show or onlyFiles):
4560                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4561                    fH.write(infoText)
4562
4563                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4564
4565                if self.useHTMLReports:
4566                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4567                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4568                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4569
4570                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4571
4572        return view
4573
4574    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4575        """
4576        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4577
4578        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4579
4580        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4581        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4582        :return: dict with raw parsed data from server and some calculated statistics about it.
4583        """
4584        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4585        tmpTicker = self._ticker
4586        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4587        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4588        self._ticker = tmpTicker
4589
4590        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4591        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4592        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4593        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4594        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4595        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4596
4597        # This is dict with parsed common user data:
4598        userInfo = {
4599            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4600            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4601            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4602            "tariff": rawUserInfo["tariff"],
4603        }
4604
4605        # This is an array of dict with parsed margin statuses for every account IDs:
4606        margins = {}
4607        for accountId in accounts.keys():
4608            if rawMargins[accountId]:
4609                margins[accountId] = {
4610                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4611                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4612                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4613                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4614                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4615                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4616                    "missing": missing["volume"],
4617                }
4618
4619            else:
4620                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4621
4622        unary = {}  # unary-connection limits
4623        for item in rawTariffLimits["unaryLimits"]:
4624            if item["limitPerMinute"] in unary.keys():
4625                unary[item["limitPerMinute"]].extend(item["methods"])
4626
4627            else:
4628                unary[item["limitPerMinute"]] = item["methods"]
4629
4630        stream = {}  # stream-connection limits
4631        for item in rawTariffLimits["streamLimits"]:
4632            if item["limit"] in stream.keys():
4633                stream[item["limit"]].extend(item["streams"])
4634
4635            else:
4636                stream[item["limit"]] = item["streams"]
4637
4638        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4639        limits = {
4640            "unary": unary,
4641            "stream": stream,
4642        }
4643
4644        # Raw and parsed data as an output result:
4645        view = {
4646            "rawUserInfo": rawUserInfo,
4647            "rawAccounts": rawAccounts,
4648            "rawMargins": rawMargins,
4649            "rawTariffLimits": rawTariffLimits,
4650            "stat": {
4651                "overview": overview,
4652                "userInfo": userInfo,
4653                "accounts": accounts,
4654                "margins": margins,
4655                "limits": limits,
4656            },
4657        }
4658
4659        # --- Prepare text table with user information in human-readable format:
4660        if show or onlyFiles:
4661            info = [
4662                "# Full user information\n\n",
4663                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4664                "## Common information\n\n",
4665                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4666                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4667                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4668                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4669                "\n## User accounts\n\n",
4670            ]
4671
4672            for account in view["stat"]["accounts"].keys():
4673                info.extend([
4674                    "### ID: [{}]\n\n".format(account),
4675                    "| Parameters           | Values                                                       |\n",
4676                    "|----------------------|--------------------------------------------------------------|\n",
4677                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4678                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4679                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4680                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4681                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4682                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4683                ])
4684
4685                if margins[account]:
4686                    info.extend([
4687                        "| Margin status:       | Enabled                                                      |\n",
4688                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4689                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4690                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4691                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4692                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4693                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4694                    ])
4695
4696                else:
4697                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4698
4699            info.extend([
4700                "\n## Current user tariff limits\n",
4701                "\n### See also\n",
4702                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4703                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4704                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4705                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4706                "\n### Unary limits\n",
4707            ])
4708
4709            if unary:
4710                for key, values in sorted(unary.items()):
4711                    info.append("\n* Max requests per minute: {}\n".format(key))
4712
4713                    for value in values:
4714                        info.append("  - {}\n".format(value))
4715
4716            else:
4717                info.append("\nNot available\n")
4718
4719            info.append("\n### Stream limits\n")
4720
4721            if stream:
4722                for key, values in sorted(stream.items()):
4723                    info.append("\n* Max stream connections: {}\n".format(key))
4724
4725                    for value in values:
4726                        info.append("  - {}\n".format(value))
4727
4728            else:
4729                info.append("\nNot available\n")
4730
4731            infoText = "".join(info)
4732
4733            if show and not onlyFiles:
4734                uLogger.info(infoText)
4735
4736            if self.userInfoFile and (show or onlyFiles):
4737                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4738                    fH.write(infoText)
4739
4740                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4741
4742                if self.useHTMLReports:
4743                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4744                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4745                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4746
4747                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4748
4749        return view
4750
4751
4752class Args:
4753    """
4754    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4755    """
4756    def __init__(self, **kwargs):
4757        self.__dict__.update(kwargs)
4758
4759    def __getattr__(self, item):
4760        return None
4761
4762
4763def ParseArgs():
4764    """This function get and parse command line keys."""
4765    parser = ArgumentParser()  # command-line string parser
4766
4767    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4768    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4769
4770    # --- options:
4771
4772    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4773    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4774    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4775
4776    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4777    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4778
4779    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4780    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4781
4782    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4783    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4784
4785    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4786    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4787    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4788
4789    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4790    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4791
4792    # --- commands:
4793
4794    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4795
4796    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4797    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4798    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4799    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4800    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4801    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4802    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4803    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4804
4805    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4806    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4807    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4808    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4809    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4810    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4811
4812    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4813    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4814    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4815    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4816
4817    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4818    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4819    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4820
4821    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4822    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4823    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4824    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4825    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4826    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4827    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4828
4829    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4830    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4831    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4832    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4833    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4834
4835    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4836    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4837    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4838
4839    cmdArgs = parser.parse_args()
4840    return cmdArgs
4841
4842
4843def Main(**kwargs):
4844    """
4845    Main function for work with TKSBrokerAPI in the console.
4846
4847    See examples:
4848    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4849    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4850    """
4851    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4852
4853    if args.debug_level:
4854        uLogger.level = 10  # always debug level by default
4855        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4856
4857    exitCode = 0
4858    start = datetime.now(tzutc())
4859    uLogger.debug("=-" * 50)
4860    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4861        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4862        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4863    ))
4864
4865    # trying to calculate full current version:
4866    buildVersion = __version__
4867    try:
4868        v = version("tksbrokerapi")
4869        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4870
4871    except Exception:
4872        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4873
4874    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4875    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4876
4877    try:
4878        if args.version:
4879            print("TKSBrokerAPI {}".format(buildVersion))
4880            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4881
4882        else:
4883            # Init class for trading with Tinkoff Broker:
4884            trader = TinkoffBrokerServer(
4885                token=args.token,
4886                accountId=args.account_id,
4887                useCache=not args.no_cache,
4888            )
4889
4890            # --- set some options:
4891
4892            if args.more:
4893                trader.moreDebug = True
4894                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4895
4896            if args.html:
4897                trader.useHTMLReports = True
4898
4899            if args.ticker:
4900                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4901
4902                if ticker in trader.aliasesKeys:
4903                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4904
4905                else:
4906                    trader.ticker = ticker
4907
4908            if args.figi:
4909                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4910
4911            if args.depth is not None:
4912                trader.depth = args.depth
4913
4914            # --- do one command:
4915
4916            if args.list:
4917                if args.output is not None:
4918                    trader.instrumentsFile = args.output
4919
4920                trader.ShowInstrumentsInfo(show=True)
4921
4922            elif args.list_xlsx:
4923                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4924
4925            elif args.bonds_xlsx is not None:
4926                if args.output is not None:
4927                    trader.bondsXLSXFile = args.output
4928
4929                if len(args.bonds_xlsx) == 0:
4930                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4931
4932                else:
4933                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4934
4935            elif args.search:
4936                if args.output is not None:
4937                    trader.searchResultsFile = args.output
4938
4939                trader.SearchInstruments(pattern=args.search[0], show=True)
4940
4941            elif args.info:
4942                if not (args.ticker or args.figi):
4943                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4944                    raise Exception("Ticker or FIGI required")
4945
4946                if args.output is not None:
4947                    trader.infoFile = args.output
4948
4949                if args.ticker:
4950                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4951
4952                else:
4953                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4954
4955            elif args.calendar is not None:
4956                if args.output is not None:
4957                    trader.calendarFile = args.output
4958
4959                if len(args.calendar) == 0:
4960                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4961
4962                else:
4963                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4964
4965                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4966
4967            elif args.price:
4968                if not (args.ticker or args.figi):
4969                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4970                    raise Exception("Ticker or FIGI required")
4971
4972                trader.GetCurrentPrices(show=True)
4973
4974            elif args.prices is not None:
4975                if args.output is not None:
4976                    trader.pricesFile = args.output
4977
4978                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4979
4980            elif args.overview:
4981                if args.output is not None:
4982                    trader.overviewFile = args.output
4983
4984                trader.Overview(show=True, details="full")
4985
4986            elif args.overview_digest:
4987                if args.output is not None:
4988                    trader.overviewDigestFile = args.output
4989
4990                trader.Overview(show=True, details="digest")
4991
4992            elif args.overview_positions:
4993                if args.output is not None:
4994                    trader.overviewPositionsFile = args.output
4995
4996                trader.Overview(show=True, details="positions")
4997
4998            elif args.overview_orders:
4999                if args.output is not None:
5000                    trader.overviewOrdersFile = args.output
5001
5002                trader.Overview(show=True, details="orders")
5003
5004            elif args.overview_analytics:
5005                if args.output is not None:
5006                    trader.overviewAnalyticsFile = args.output
5007
5008                trader.Overview(show=True, details="analytics")
5009
5010            elif args.overview_calendar:
5011                if args.output is not None:
5012                    trader.overviewAnalyticsFile = args.output
5013
5014                trader.Overview(show=True, details="calendar")
5015
5016            elif args.deals is not None:
5017                if args.output is not None:
5018                    trader.reportFile = args.output
5019
5020                if 0 <= len(args.deals) < 3:
5021                    trader.Deals(
5022                        start=args.deals[0] if len(args.deals) >= 1 else None,
5023                        end=args.deals[1] if len(args.deals) == 2 else None,
5024                        show=True,  # Always show deals report in console
5025                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5026                    )
5027
5028                else:
5029                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5030                    raise Exception("Incorrect value")
5031
5032            elif args.history is not None:
5033                if args.output is not None:
5034                    trader.historyFile = args.output
5035
5036                if 0 <= len(args.history) < 3:
5037                    dataReceived = trader.History(
5038                        start=args.history[0] if len(args.history) >= 1 else None,
5039                        end=args.history[1] if len(args.history) == 2 else None,
5040                        interval="hour" if args.interval is None or not args.interval else args.interval,
5041                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5042                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5043                        show=True,  # shows all downloaded candles in console
5044                    )
5045
5046                    if args.render_chart is not None and dataReceived is not None:
5047                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5048
5049                        trader.ShowHistoryChart(
5050                            candles=dataReceived,
5051                            interact=iChart,
5052                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5053                        )
5054
5055                else:
5056                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5057                    raise Exception("Incorrect value")
5058
5059            elif args.load_history is not None:
5060                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5061
5062                if args.render_chart is not None and histData is not None:
5063                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5064                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5065
5066                    trader.ShowHistoryChart(
5067                        candles=histData,
5068                        interact=iChart,
5069                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5070                    )
5071
5072            elif args.trade is not None:
5073                if 1 <= len(args.trade) <= 5:
5074                    trader.Trade(
5075                        operation=args.trade[0],
5076                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5077                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5078                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5079                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5080                    )
5081
5082                else:
5083                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5084
5085            elif args.buy is not None:
5086                if 0 <= len(args.buy) <= 4:
5087                    trader.Buy(
5088                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5089                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5090                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5091                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5092                    )
5093
5094                else:
5095                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5096
5097            elif args.sell is not None:
5098                if 0 <= len(args.sell) <= 4:
5099                    trader.Sell(
5100                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5101                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5102                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5103                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5104                    )
5105
5106                else:
5107                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5108
5109            elif args.order:
5110                if 4 <= len(args.order) <= 7:
5111                    trader.Order(
5112                        operation=args.order[0],
5113                        orderType=args.order[1],
5114                        lots=int(args.order[2]),
5115                        targetPrice=float(args.order[3]),
5116                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5117                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5118                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5119                    )
5120
5121                else:
5122                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5123
5124            elif args.buy_limit:
5125                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5126
5127            elif args.sell_limit:
5128                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5129
5130            elif args.buy_stop:
5131                if 2 <= len(args.buy_stop) <= 7:
5132                    trader.BuyStop(
5133                        lots=int(args.buy_stop[0]),
5134                        targetPrice=float(args.buy_stop[1]),
5135                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5136                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5137                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5138                    )
5139
5140                else:
5141                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5142
5143            elif args.sell_stop:
5144                if 2 <= len(args.sell_stop) <= 7:
5145                    trader.SellStop(
5146                        lots=int(args.sell_stop[0]),
5147                        targetPrice=float(args.sell_stop[1]),
5148                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5149                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5150                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5151                    )
5152
5153                else:
5154                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5155
5156            # elif args.buy_order_grid is not None:
5157            #     # update order grid work with api v2
5158            #     if len(args.buy_order_grid) == 2:
5159            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5160            #
5161            #         for order in orderParams:
5162            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5163            #
5164            #     else:
5165            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5166            #
5167            # elif args.sell_order_grid is not None:
5168            #     # update order grid work with api v2
5169            #     if len(args.sell_order_grid) >= 2:
5170            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5171            #
5172            #         for order in orderParams:
5173            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5174            #
5175            #     else:
5176            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5177
5178            elif args.close_order is not None:
5179                trader.CloseOrders(args.close_order)  # close only one order
5180
5181            elif args.close_orders is not None:
5182                trader.CloseOrders(args.close_orders)  # close list of orders
5183
5184            elif args.close_trade:
5185                if not (args.ticker or args.figi):
5186                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5187                    raise Exception("Ticker or FIGI required")
5188
5189                if args.ticker:
5190                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5191
5192                else:
5193                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5194
5195            elif args.close_trades is not None:
5196                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5197
5198            elif args.close_all is not None:
5199                if args.ticker:
5200                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5201
5202                elif args.figi:
5203                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5204
5205                else:
5206                    trader.CloseAll(*args.close_all)
5207
5208            elif args.limits:
5209                if args.output is not None:
5210                    trader.withdrawalLimitsFile = args.output
5211
5212                trader.OverviewLimits(show=True)
5213
5214            elif args.user_info:
5215                if args.output is not None:
5216                    trader.userInfoFile = args.output
5217
5218                trader.OverviewUserInfo(show=True)
5219
5220            elif args.account:
5221                if args.output is not None:
5222                    trader.userAccountsFile = args.output
5223
5224                trader.OverviewAccounts(show=True)
5225
5226            else:
5227                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5228                raise Exception("There is no command to execute")
5229
5230    except Exception:
5231        trace = tb.format_exc()
5232        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5233            if e in trace:
5234                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5235                break
5236
5237        uLogger.debug(trace)
5238        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5239        exitCode = 255  # an error occurred, must be open a ticket for this issue
5240
5241    finally:
5242        finish = datetime.now(tzutc())
5243
5244        if exitCode == 0:
5245            if args.more:
5246                uLogger.debug("All operations were finished success (summary code is 0).")
5247
5248        else:
5249            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5250                os.path.abspath(uLog.defaultLogFile), exitCode,
5251            ))
5252
5253        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5254        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5255            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5256            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5257        ))
5258        uLogger.debug("=-" * 50)
5259
5260        if not kwargs:
5261            sys.exit(exitCode)
5262
5263        else:
5264            return exitCode
5265
5266
5267if __name__ == "__main__":
5268    Main()
class TinkoffBrokerServer:
  78class TinkoffBrokerServer:
  79    """
  80    This class implements methods to work with Tinkoff broker server.
  81
  82    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  83
  84    About `token`: https://tinkoff.github.io/investAPI/token/
  85    """
  86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  87        """
  88        Main class init.
  89
  90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  93        :param useCache: use default cache file with raw data to use instead of `iList`.
  94                         True by default. Cache is auto-update if new day has come.
  95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  96        :param defaultCache: path to default cache file. `dump.json` by default.
  97        """
  98        if token is None or not token:
  99            try:
 100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 102
 103            except KeyError:
 104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 105                raise Exception("Token required")
 106
 107        else:
 108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 110
 111        if accountId is None or not accountId:
 112            try:
 113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 115
 116            except KeyError:
 117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 118
 119        else:
 120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 122
 123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 125
 126        Latest version: https://pypi.org/project/tksbrokerapi/
 127        """
 128
 129        self.__lock = Lock()  # initialize multiprocessing mutex lock
 130
 131        self.aliases = TKS_TICKER_ALIASES
 132        """Some aliases instead official tickers.
 133
 134        See also: `TKSEnums.TKS_TICKER_ALIASES`
 135        """
 136
 137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 138
 139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 140
 141        self._ticker = ""
 142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 143
 144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 146
 147        See also: `SearchByTicker()`, `SearchInstruments()`.
 148        """
 149
 150        self._figi = ""
 151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 152
 153        See also: `SearchByFIGI()`, `SearchInstruments()`.
 154        """
 155
 156        self.depth = 1
 157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 158
 159        See also: `GetCurrentPrices()`.
 160        """
 161
 162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 164
 165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 166        """
 167
 168        uLogger.debug("Broker API server: {}".format(self.server))
 169
 170        self.timeout = 15
 171        """Server operations timeout in seconds. Default: `15`.
 172
 173        See also: `SendAPIRequest()`.
 174        """
 175
 176        self.headers = {
 177            "Content-Type": "application/json",
 178            "accept": "application/json",
 179            "Authorization": "Bearer {}".format(self.token),
 180            "x-app-name": "Tim55667757.TKSBrokerAPI",
 181        }
 182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 183
 184        See also: `SendAPIRequest()`.
 185        """
 186
 187        self.body = None
 188        """Request body which send to broker server. Default: `None`.
 189
 190        See also: `SendAPIRequest()`.
 191        """
 192
 193        self.moreDebug = False
 194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 195
 196        self.useHTMLReports = False
 197        """
 198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 199        
 200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 201        """
 202
 203        self.historyFile = None
 204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 205
 206        See also: `History()`.
 207        """
 208
 209        self.htmlHistoryFile = "index.html"
 210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 211
 212        See also: `ShowHistoryChart()`.
 213        """
 214
 215        self.instrumentsFile = "instruments.md"
 216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 217
 218        See also: `ShowInstrumentsInfo()`.
 219        """
 220
 221        self.searchResultsFile = "search-results.md"
 222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 223
 224        See also: `SearchInstruments()`.
 225        """
 226
 227        self.pricesFile = "prices.md"
 228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 229
 230        See also: `GetListOfPrices()`.
 231        """
 232
 233        self.infoFile = "info.md"
 234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 235
 236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 237        """
 238
 239        self.bondsXLSXFile = "ext-bonds.xlsx"
 240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 242
 243        See also: `ExtendBondsData()`.
 244        """
 245
 246        self.calendarFile = "calendar.md"
 247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 248        
 249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 250
 251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 252        """
 253
 254        self.overviewFile = "overview.md"
 255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 256
 257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 258        """
 259
 260        self.overviewDigestFile = "overview-digest.md"
 261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 262
 263        See also: `Overview()` with parameter `details="digest"`.
 264        """
 265
 266        self.overviewPositionsFile = "overview-positions.md"
 267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 268
 269        See also: `Overview()` with parameter `details="positions"`.
 270        """
 271
 272        self.overviewOrdersFile = "overview-orders.md"
 273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 274
 275        See also: `Overview()` with parameter `details="orders"`.
 276        """
 277
 278        self.overviewAnalyticsFile = "overview-analytics.md"
 279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 280
 281        See also: `Overview()` with parameter `details="analytics"`.
 282        """
 283
 284        self.overviewBondsCalendarFile = "overview-calendar.md"
 285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 286
 287        See also: `Overview()` with parameter `details="calendar"`.
 288        """
 289
 290        self.reportFile = "deals.md"
 291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 292
 293        See also: `Deals()`.
 294        """
 295
 296        self.withdrawalLimitsFile = "limits.md"
 297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 298
 299        See also: `OverviewLimits()` and `RequestLimits()`.
 300        """
 301
 302        self.userInfoFile = "user-info.md"
 303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 304
 305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 306        """
 307
 308        self.userAccountsFile = "accounts.md"
 309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 310
 311        See also: `OverviewAccounts()`, `RequestAccounts()`.
 312        """
 313
 314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 316
 317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 318
 319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 320        """
 321
 322        self.iList = None  # init iList for raw instruments data
 323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 324        
 325        See also: `Listing()`, `DumpInstruments()`.
 326        """
 327
 328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 329        if useCache:
 330            if os.path.exists(self.iListDumpFile):
 331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 332                curTime = datetime.now(tzutc())
 333
 334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 336
 337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 338
 339                else:
 340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 341
 342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 343                        os.path.abspath(self.iListDumpFile),
 344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 345                    ))
 346
 347            else:
 348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 350
 351        else:
 352            self.iList = self.Listing()  # request new raw instruments data from broker server
 353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 354
 355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 357
 358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 359        """
 360
 361    @property
 362    def ticker(self) -> str:
 363        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 364
 365        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 366        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 367
 368        See also: `SearchByTicker()`, `SearchInstruments()`.
 369        """
 370        return self._ticker
 371
 372    @ticker.setter
 373    def ticker(self, value):
 374        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 375
 376        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 377        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 378
 379        See also: `SearchByTicker()`, `SearchInstruments()`.
 380        """
 381        self._ticker = str(value).upper()  # Tickers may be upper case only
 382
 383    @property
 384    def figi(self) -> str:
 385        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 386
 387        See also: `SearchByFIGI()`, `SearchInstruments()`.
 388        """
 389        return self._figi
 390
 391    @figi.setter
 392    def figi(self, value):
 393        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 394
 395        See also: `SearchByFIGI()`, `SearchInstruments()`.
 396        """
 397        self._figi = str(value).upper()  # FIGI may be upper case only
 398
 399    def _ParseJSON(self, rawData="{}") -> dict:
 400        """
 401        Parse JSON from response string.
 402
 403        :param rawData: this is a string with JSON-formatted text.
 404        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 405        """
 406        try:
 407            responseJSON = json.loads(rawData) if rawData else {}
 408
 409            if self.moreDebug:
 410                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 411
 412            return responseJSON
 413
 414        except Exception as e:
 415            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 416
 417            return {}
 418
 419    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 420        """
 421        Send GET or POST request to broker server and receive JSON object.
 422
 423        self.header: must be defining with dictionary of headers.
 424        self.body: if define then used as request body. None by default.
 425        self.timeout: global request timeout, 15 seconds by default.
 426        :param url: url with REST request.
 427        :param reqType: send "GET" or "POST" request. "GET" by default.
 428        :param retry: how many times retry after first request if an 5xx server errors occurred.
 429        :param pause: sleep time in seconds between retries.
 430        :return: response JSON (dictionary) from broker.
 431        """
 432        if reqType.upper() not in ("GET", "POST"):
 433            uLogger.error("You can define request type: `GET` or `POST`!")
 434            raise Exception("Incorrect value")
 435
 436        if self.moreDebug:
 437            uLogger.debug("Request parameters:")
 438            uLogger.debug("    - REST API URL: {}".format(url))
 439            uLogger.debug("    - request type: {}".format(reqType))
 440            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 441            uLogger.debug("    - body:\n{}".format(self.body))
 442
 443        # fast hack to avoid all operations with some tickers/FIGI
 444        responseJSON = {}
 445        oK = True
 446        for item in self.exclude:
 447            if item in url:
 448                if self.moreDebug:
 449                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 450
 451                oK = False
 452                break
 453
 454        if oK:
 455            with self.__lock:  # acquire the mutex lock
 456                counter = 0
 457                response = None
 458                errMsg = ""
 459
 460                while not response and counter <= retry:
 461                    if reqType == "GET":
 462                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 463
 464                    if reqType == "POST":
 465                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 466
 467                    if self.moreDebug:
 468                        uLogger.debug("Response:")
 469                        uLogger.debug("    - status code: {}".format(response.status_code))
 470                        uLogger.debug("    - reason: {}".format(response.reason))
 471                        uLogger.debug("    - body length: {}".format(len(response.text)))
 472                        uLogger.debug("    - headers:\n{}".format(response.headers))
 473
 474                    # Server returns some headers:
 475                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 476                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 477                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 478                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 479                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 480                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 481                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 482                        sleep(rateLimitWait)
 483
 484                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 485                    if 400 <= response.status_code < 500:
 486                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 487                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 488
 489                        if "code" in response.text and "message" in response.text:
 490                            msgDict = self._ParseJSON(rawData=response.text)
 491                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 492
 493                        counter = retry + 1  # do not retry for 4xx errors
 494
 495                    if 500 <= response.status_code < 600:
 496                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 497                        uLogger.debug("    - not oK, {}".format(errMsg))
 498
 499                        if "code" in response.text and "message" in response.text:
 500                            errMsgDict = self._ParseJSON(rawData=response.text)
 501                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 502
 503                        counter += 1
 504
 505                        if counter <= retry:
 506                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 507                            sleep(pause)
 508
 509                responseJSON = self._ParseJSON(rawData=response.text)
 510
 511                if errMsg:
 512                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 513                    uLogger.error("    - not oK, {}".format(errMsg))
 514
 515        return responseJSON
 516
 517    def _IUpdater(self, iType: str) -> tuple:
 518        """
 519        Request instrument by type from server. See available API methods for instruments:
 520        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 521        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 522        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 523        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 524        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 525
 526        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 527        :return: tuple with iType name and list of available instruments of current type for defined user token.
 528        """
 529        result = []
 530
 531        if iType in TKS_INSTRUMENTS:
 532            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 533
 534            # all instruments have the same body in API v2 requests:
 535            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 536            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 537            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 538
 539        return iType, result
 540
 541    def _IWrapper(self, kwargs):
 542        """
 543        Wrapper runs instrument's update method `_IUpdater()`.
 544        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 545        """
 546        return self._IUpdater(**kwargs)
 547
 548    def Listing(self) -> dict:
 549        """
 550        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 551
 552        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 553        """
 554        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 555        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 556
 557        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 558        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 559        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 560
 561        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 562        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 563        poolUpdater.close()  # close the thread pool
 564        poolUpdater.join()  # wait a moment until all data returns from threads
 565
 566        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 567        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 568        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 569
 570        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 571        for iType in iList.keys():
 572            for ticker in iList[iType]:
 573                iList[iType][ticker]["type"] = iType
 574
 575                if "minPriceIncrement" in iList[iType][ticker].keys():
 576                    iList[iType][ticker]["step"] = NanoToFloat(
 577                        iList[iType][ticker]["minPriceIncrement"]["units"],
 578                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 579                    )
 580
 581                else:
 582                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 583
 584        return iList
 585
 586    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 587        """
 588        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 589
 590        See also: `DumpInstruments()`, `Listing()`.
 591
 592        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 593                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 594        """
 595        if self.iListDumpFile is None or not self.iListDumpFile:
 596            uLogger.error("Output name of dump file must be defined!")
 597            raise Exception("Filename required")
 598
 599        if not self.iList or forceUpdate:
 600            self.iList = self.Listing()
 601
 602        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 603
 604        # Save as XLSX with separated sheets for every type of instruments:
 605        with pd.ExcelWriter(
 606                path=xlsxDumpFile,
 607                date_format=TKS_DATE_FORMAT,
 608                datetime_format=TKS_DATE_TIME_FORMAT,
 609                mode="w",
 610        ) as writer:
 611            for iType in TKS_INSTRUMENTS:
 612                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 613                df = df[sorted(df)]  # sorted by column names
 614                df = df.applymap(
 615                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 616                    na_action="ignore",
 617                )  # converting numbers from nano-type to float in every cell
 618                df.to_excel(
 619                    writer,
 620                    sheet_name=iType,
 621                    encoding="UTF-8",
 622                    freeze_panes=(1, 1),
 623                )  # saving as XLSX-file with freeze first row and column as headers
 624
 625        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 626
 627    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 628        """
 629        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 630        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 631
 632        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 633
 634        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 635                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 636        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 637        """
 638        if self.iListDumpFile is None or not self.iListDumpFile:
 639            uLogger.error("Output name of dump file must be defined!")
 640            raise Exception("Filename required")
 641
 642        if not self.iList or forceUpdate:
 643            self.iList = self.Listing()
 644
 645        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 646        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 647            fH.write(jsonDump)
 648
 649        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 650
 651        return jsonDump
 652
 653    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
 654        """
 655        Show information about one instrument defined by json data and prints it in Markdown format.
 656
 657        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 658
 659        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 660        :param show: if `True` then also printing information about instrument and its current price.
 661        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
 662        :return: multilines text in Markdown format with information about one instrument.
 663        """
 664        splitLine = "|                                                             |                                                        |\n"
 665        infoText = ""
 666
 667        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 668            info = [
 669                "# Main information\n\n",
 670                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 671                "| Parameters                                                  | Values                                                 |\n",
 672                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 673                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 674                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 675            ]
 676
 677            if "sector" in iJSON.keys() and iJSON["sector"]:
 678                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 679
 680            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 681                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 682
 683            info.extend([
 684                splitLine,
 685                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 686                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 687            ])
 688
 689            if "isin" in iJSON.keys() and iJSON["isin"]:
 690                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 691
 692            if "classCode" in iJSON.keys():
 693                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 694
 695            info.extend([
 696                splitLine,
 697                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 698                splitLine,
 699                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 700                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 701                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 702            ])
 703
 704            if iJSON["figi"]:
 705                self._figi = iJSON["figi"]
 706                iJSON = iJSON | self.RequestTradingStatus()
 707
 708                info.extend([
 709                    splitLine,
 710                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 711                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 712                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 713                ])
 714
 715            info.append(splitLine)
 716
 717            if "type" in iJSON.keys() and iJSON["type"]:
 718                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 719
 720                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 721                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 722
 723            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 724                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 725
 726            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 727                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 728
 729            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 730                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 731
 732            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 733                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 734
 735            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 736                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 737
 738            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 739                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 740
 741            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 742                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 743
 744            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 745                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 746
 747            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 748                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 749
 750            if "currency" in iJSON.keys():
 751                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 752
 753            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 754                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 755
 756            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 757                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 758
 759            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 760                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 761
 762            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 763                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 764
 765            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 766                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 767
 768            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 769                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 770
 771            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 772                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 773
 774            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 775                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 776
 777            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 778                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 779
 780            iExt = None
 781            if iJSON["type"] == "Bonds":
 782                info.extend([
 783                    splitLine,
 784                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 785                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 786                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 787                        iJSON["nominal"]["currency"],
 788                    )),
 789                ])
 790
 791                if "floatingCouponFlag" in iJSON.keys():
 792                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 793
 794                if "amortizationFlag" in iJSON.keys():
 795                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 796
 797                info.append(splitLine)
 798
 799                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 800                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 801
 802                if iJSON["figi"]:
 803                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 804
 805                    info.extend([
 806                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 807                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 808                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 809                    ])
 810
 811                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 812                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 813                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 814                        iJSON["aciValue"]["currency"]
 815                    )))
 816
 817            if "currentPrice" in iJSON.keys():
 818                info.append(splitLine)
 819
 820                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 821                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 822
 823                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 824                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 825                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 826                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 827                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 828
 829                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 830                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 831
 832                info.extend([
 833                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 834                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 835                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 836                    )),
 837                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 838                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 839                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 840                    )),
 841                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 842                        "{:.2f}%{}".format(
 843                            iJSON["currentPrice"]["changes"],
 844                            " ({}{:.2f} {})".format(
 845                                "+" if bondChangesDelta > 0 else "",
 846                                bondChangesDelta,
 847                                aciCurrency
 848                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 849                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 850                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 851                                currency
 852                            ),
 853                        )
 854                    ),
 855                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 856                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 857                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 858                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 859                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 860                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 861                    )),
 862                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 863                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 864                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 865                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 866                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 867                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 868                    )),
 869                ])
 870
 871            if "lot" in iJSON.keys():
 872                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 873
 874            if "step" in iJSON.keys() and iJSON["step"] != 0:
 875                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 876
 877            # Add bond payment calendar:
 878            if iJSON["type"] == "Bonds":
 879                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 880                info.extend(["\n#", strCalendar])
 881
 882            infoText += "".join(info)
 883
 884            if show and not onlyFiles:
 885                uLogger.info("{}".format(infoText))
 886
 887            if self.infoFile is not None and (show or onlyFiles):
 888                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 889                    fH.write(infoText)
 890
 891                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 892
 893                if self.useHTMLReports:
 894                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 895                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 896                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 897
 898                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 899
 900        return infoText
 901
 902    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 903        """
 904        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 905
 906        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 907        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 908        :return: JSON formatted data with information about instrument.
 909        """
 910        tickerJSON = {}
 911        if self.moreDebug:
 912            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 913
 914        if not self._ticker:
 915            uLogger.warning("self._ticker variable is not be empty!")
 916
 917        else:
 918            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 919                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 920                raise Exception("Instrument not allowed")
 921
 922            if not self.iList:
 923                self.iList = self.Listing()
 924
 925            if self._ticker in self.iList["Shares"].keys():
 926                tickerJSON = self.iList["Shares"][self._ticker]
 927                if self.moreDebug:
 928                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 929
 930            elif self._ticker in self.iList["Currencies"].keys():
 931                tickerJSON = self.iList["Currencies"][self._ticker]
 932                if self.moreDebug:
 933                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 934
 935            elif self._ticker in self.iList["Bonds"].keys():
 936                tickerJSON = self.iList["Bonds"][self._ticker]
 937                if self.moreDebug:
 938                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 939
 940            elif self._ticker in self.iList["Etfs"].keys():
 941                tickerJSON = self.iList["Etfs"][self._ticker]
 942                if self.moreDebug:
 943                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 944
 945            elif self._ticker in self.iList["Futures"].keys():
 946                tickerJSON = self.iList["Futures"][self._ticker]
 947                if self.moreDebug:
 948                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 949
 950        if tickerJSON:
 951            self._figi = tickerJSON["figi"]
 952
 953            if requestPrice:
 954                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 955
 956                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 957                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 958
 959                else:
 960                    tickerJSON["currentPrice"]["changes"] = 0
 961
 962            if show:
 963                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 964
 965        else:
 966            if show:
 967                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 968
 969        return tickerJSON
 970
 971    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 972        """
 973        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 974
 975        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 976        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 977        :return: JSON formatted data with information about instrument.
 978        """
 979        figiJSON = {}
 980        if self.moreDebug:
 981            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 982
 983        if not self._figi:
 984            uLogger.warning("self._figi variable is not be empty!")
 985
 986        else:
 987            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 988                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 989                raise Exception("Instrument not allowed")
 990
 991            if not self.iList:
 992                self.iList = self.Listing()
 993
 994            for item in self.iList["Shares"].keys():
 995                if self._figi == self.iList["Shares"][item]["figi"]:
 996                    figiJSON = self.iList["Shares"][item]
 997
 998                    if self.moreDebug:
 999                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1000
1001                    break
1002
1003            if not figiJSON:
1004                for item in self.iList["Currencies"].keys():
1005                    if self._figi == self.iList["Currencies"][item]["figi"]:
1006                        figiJSON = self.iList["Currencies"][item]
1007
1008                        if self.moreDebug:
1009                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1010
1011                        break
1012
1013            if not figiJSON:
1014                for item in self.iList["Bonds"].keys():
1015                    if self._figi == self.iList["Bonds"][item]["figi"]:
1016                        figiJSON = self.iList["Bonds"][item]
1017
1018                        if self.moreDebug:
1019                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1020
1021                        break
1022
1023            if not figiJSON:
1024                for item in self.iList["Etfs"].keys():
1025                    if self._figi == self.iList["Etfs"][item]["figi"]:
1026                        figiJSON = self.iList["Etfs"][item]
1027
1028                        if self.moreDebug:
1029                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1030
1031                        break
1032
1033            if not figiJSON:
1034                for item in self.iList["Futures"].keys():
1035                    if self._figi == self.iList["Futures"][item]["figi"]:
1036                        figiJSON = self.iList["Futures"][item]
1037
1038                        if self.moreDebug:
1039                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1040
1041                        break
1042
1043        if figiJSON:
1044            self._figi = figiJSON["figi"]
1045            self._ticker = figiJSON["ticker"]
1046
1047            if requestPrice:
1048                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1049
1050                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1051                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1052
1053                else:
1054                    figiJSON["currentPrice"]["changes"] = 0
1055
1056            if show:
1057                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1058
1059        else:
1060            if show:
1061                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1062
1063        return figiJSON
1064
1065    def GetCurrentPrices(self, show: bool = True) -> dict:
1066        """
1067        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1068        `{"buy": [{"price": 1243.8, "quantity": 193},
1069                  {"price": 1244.0, "quantity": 168},
1070                  {"price": 1244.8, "quantity": 5},
1071                  {"price": 1245.0, "quantity": 61},
1072                  {"price": 1245.4, "quantity": 60}],
1073          "sell": [{"price": 1243.6, "quantity": 8},
1074                   {"price": 1242.6, "quantity": 10},
1075                   {"price": 1242.4, "quantity": 18},
1076                   {"price": 1242.2, "quantity": 50},
1077                   {"price": 1242.0, "quantity": 113}],
1078          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1079        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1080        - sell: list of dicts with Buyers prices,
1081            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1082            - quantity: volume value by current price in lots,
1083        - limitUp: current trade session limit price, maximum,
1084        - limitDown: current trade session limit price, minimum,
1085        - lastPrice: last deal price of the instrument,
1086        - closePrice: previous trade session close price of the instrument.
1087
1088        See also: `SearchByTicker()` and `SearchByFIGI()`.
1089        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1090        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1091
1092        :param show: if `True` then print DOM to log and console.
1093        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1094                 If an error occurred then returns an empty record:
1095                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1096        """
1097        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1098
1099        if self.depth < 1:
1100            uLogger.error("Depth of Market (DOM) must be >=1!")
1101            raise Exception("Incorrect value")
1102
1103        if not (self._ticker or self._figi):
1104            uLogger.error("self._ticker or self._figi variables must be defined!")
1105            raise Exception("Ticker or FIGI required")
1106
1107        if self._ticker and not self._figi:
1108            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1109            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1110
1111        if not self._ticker and self._figi:
1112            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1113            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1114
1115        if not self._figi:
1116            uLogger.error("FIGI is not defined!")
1117            raise Exception("Ticker or FIGI required")
1118
1119        else:
1120            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1121
1122            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1123            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1124            self.body = str({"figi": self._figi, "depth": self.depth})
1125            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1126
1127            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1128                # list of dicts with sellers orders:
1129                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1130
1131                # list of dicts with buyers orders:
1132                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1133
1134                # max price of instrument at this time:
1135                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1136
1137                # min price of instrument at this time:
1138                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1139
1140                # last price of deal with instrument:
1141                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1142
1143                # last close price of instrument:
1144                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1145
1146            else:
1147                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1148                uLogger.debug("Server response: {}".format(pricesResponse))
1149
1150            if show:
1151                if prices["buy"] or prices["sell"]:
1152                    info = [
1153                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1154                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1155                            self._ticker,
1156                            self._figi,
1157                            self.depth,
1158                        ),
1159                        "-" * 60, "\n",
1160                        "             Orders of Buyers | Orders of Sellers\n",
1161                        "-" * 60, "\n",
1162                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1163                        "-" * 60, "\n",
1164                    ]
1165
1166                    if not prices["buy"]:
1167                        info.append("                              | No orders!\n")
1168                        sumBuy = 0
1169
1170                    else:
1171                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1172                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1173                        for item in maxMinSorted:
1174                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1175
1176                    if not prices["sell"]:
1177                        info.append("No orders!                    |\n")
1178                        sumSell = 0
1179
1180                    else:
1181                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1182                        for item in prices["sell"]:
1183                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1184
1185                    info.extend([
1186                        "-" * 60, "\n",
1187                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1188                        "-" * 60, "\n",
1189                    ])
1190
1191                    infoText = "".join(info)
1192
1193                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1194
1195                else:
1196                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1197
1198        return prices
1199
1200    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1201        """
1202        This method get and show information about all available broker instruments for current user account.
1203        If `instrumentsFile` string is not empty then also save information to this file.
1204
1205        :param show: if `True` then print results to console, if `False` — print only to file.
1206        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1207        :return: multi-lines string with all available broker instruments.
1208        """
1209        if not self.iList:
1210            self.iList = self.Listing()
1211
1212        info = [
1213            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1214            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1215        ]
1216
1217        # add instruments count by type:
1218        for iType in self.iList.keys():
1219            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1220
1221        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1222        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1223
1224        # generating info tables with all instruments by type:
1225        for iType in self.iList.keys():
1226            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1227
1228            for instrument in self.iList[iType].keys():
1229                iName = self.iList[iType][instrument]["name"]  # instrument's name
1230                if len(iName) > 57:
1231                    iName = "{}...".format(iName[:54])  # right trim for a long string
1232
1233                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1234                    self.iList[iType][instrument]["ticker"],
1235                    iName,
1236                    self.iList[iType][instrument]["figi"],
1237                    self.iList[iType][instrument]["currency"],
1238                    self.iList[iType][instrument]["lot"],
1239                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1240                ))
1241
1242        infoText = "".join(info)
1243
1244        if show and not onlyFiles:
1245            uLogger.info(infoText)
1246
1247        if self.instrumentsFile and (show or onlyFiles):
1248            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1249                fH.write(infoText)
1250
1251            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1252
1253            if self.useHTMLReports:
1254                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1255                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1256                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1257
1258                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1259
1260        return infoText
1261
1262    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1263        """
1264        This method search and show information about instruments by part of its ticker, FIGI or name.
1265        If `searchResultsFile` string is not empty then also save information to this file.
1266
1267        :param pattern: string with part of ticker, FIGI or instrument's name.
1268        :param show: if `True` then print results to console, if `False` — return list of result only.
1269        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1270        :return: list of dictionaries with all found instruments.
1271        """
1272        if not self.iList:
1273            self.iList = self.Listing()
1274
1275        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1276        compiledPattern = re.compile(pattern, re.IGNORECASE)
1277
1278        for iType in self.iList:
1279            for instrument in self.iList[iType].values():
1280                searchResult = compiledPattern.search(" ".join(
1281                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1282                ))
1283
1284                if searchResult:
1285                    searchResults[iType][instrument["ticker"]] = instrument
1286
1287        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1288        info = [
1289            "# Search results\n\n",
1290            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1291            "* **Search pattern:** [{}]\n".format(pattern),
1292            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1293            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1294        ]
1295        infoShort = info[:]
1296
1297        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1298        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1299        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1300
1301        if resultsLen == 0:
1302            info.append("\nNo results\n")
1303            infoShort.append("\nNo results\n")
1304            uLogger.warning("No results. Try changing your search pattern.")
1305
1306        else:
1307            for iType in searchResults:
1308                iTypeValuesCount = len(searchResults[iType].values())
1309                if iTypeValuesCount > 0:
1310                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1311                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1312
1313                    for instrument in searchResults[iType].values():
1314                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1315                            instrument["type"],
1316                            instrument["ticker"],
1317                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1318                            instrument["figi"],
1319                        ))
1320
1321                    if iTypeValuesCount <= 5:
1322                        infoShort.extend(info[-iTypeValuesCount:])
1323
1324                    else:
1325                        infoShort.extend(info[-5:])
1326                        infoShort.append(skippedLine)
1327
1328        infoText = "".join(info)
1329        infoTextShort = "".join(infoShort)
1330
1331        if show and not onlyFiles:
1332            uLogger.info(infoTextShort)
1333            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1334
1335        if self.searchResultsFile and (show or onlyFiles):
1336            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1337                fH.write(infoText)
1338
1339            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1340
1341            if self.useHTMLReports:
1342                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1343                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1344                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1345
1346                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1347
1348        return searchResults
1349
1350    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1351        """
1352        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1353
1354        :param instruments: list of strings with tickers or FIGIs.
1355        :return: list with unique instrument FIGIs only.
1356        """
1357        requestedInstruments = []
1358        for iName in instruments:
1359            if iName not in self.aliases.keys():
1360                if iName not in requestedInstruments:
1361                    requestedInstruments.append(iName)
1362
1363            else:
1364                if iName not in requestedInstruments:
1365                    if self.aliases[iName] not in requestedInstruments:
1366                        requestedInstruments.append(self.aliases[iName])
1367
1368        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1369
1370        onlyUniqueFIGIs = []
1371        for iName in requestedInstruments:
1372            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1373                continue
1374
1375            self._ticker = iName
1376            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1377
1378            if not iData:
1379                self._ticker = ""
1380                self._figi = iName
1381
1382                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1383
1384                if not iData:
1385                    self._figi = ""
1386                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1387
1388            if iData and iData["figi"] not in onlyUniqueFIGIs:
1389                onlyUniqueFIGIs.append(iData["figi"])
1390
1391        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1392
1393        return onlyUniqueFIGIs
1394
1395    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1396        """
1397        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1398
1399        See limits: https://tinkoff.github.io/investAPI/limits/
1400
1401        If `pricesFile` string is not empty then also save information to this file.
1402
1403        :param instruments: list of strings with tickers or FIGIs.
1404        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1405        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1406        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1407                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1408        """
1409        if instruments is None or not instruments:
1410            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1411            raise Exception("Ticker or FIGI required")
1412
1413        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1414
1415        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1416
1417        iList = []  # trying to get info and current prices about all unique instruments:
1418        for self._figi in onlyUniqueFIGIs:
1419            iData = self.SearchByFIGI(requestPrice=True, show=False)
1420            iList.append(iData)
1421
1422        self.ShowListOfPrices(iList, show, onlyFiles)
1423
1424        return iList
1425
1426    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1427        """
1428        Show table contains current prices of given instruments.
1429
1430        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1431                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1432        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1433        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1434        :return: multilines text in Markdown format as a table contains current prices.
1435        """
1436        infoText = ""
1437
1438        if show or self.pricesFile or onlyFiles:
1439            info = [
1440                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1441                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1442                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1443            ]
1444
1445            for item in iList:
1446                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1447                    item["ticker"],
1448                    item["figi"],
1449                    item["type"],
1450                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1451                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1452                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1453                    "{} / {}".format(
1454                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1455                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1456                    ),
1457                    "{} / {}".format(
1458                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1459                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1460                    ),
1461                    item["currency"],
1462                ))
1463
1464            infoText = "".join(info)
1465
1466            if show and not onlyFiles:
1467                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1468
1469            if self.pricesFile and (show or onlyFiles):
1470                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1471                    fH.write(infoText)
1472
1473                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1474
1475                if self.useHTMLReports:
1476                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1477                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1478                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1479
1480                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1481
1482        return infoText
1483
1484    def RequestTradingStatus(self) -> dict:
1485        """
1486        Requesting trading status for the instrument defined by `figi` variable.
1487
1488        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1489
1490        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1491
1492        :return: dictionary with trading status attributes. Response example:
1493                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1494                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1495        """
1496        if self._figi is None or not self._figi:
1497            uLogger.error("Variable `figi` must be defined for using this method!")
1498            raise Exception("FIGI required")
1499
1500        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1501
1502        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1503        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1504        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1505
1506        if self.moreDebug:
1507            uLogger.debug("Records about current trading status successfully received")
1508
1509        return tradingStatus
1510
1511    def RequestPortfolio(self) -> dict:
1512        """
1513        Requesting actual user's portfolio for current `accountId`.
1514
1515        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1516
1517        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1518
1519        :return: dictionary with user's portfolio.
1520        """
1521        if self.accountId is None or not self.accountId:
1522            uLogger.error("Variable `accountId` must be defined for using this method!")
1523            raise Exception("Account ID required")
1524
1525        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1526
1527        self.body = str({"accountId": self.accountId})
1528        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1529        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1530
1531        if self.moreDebug:
1532            uLogger.debug("Records about user's portfolio successfully received")
1533
1534        return rawPortfolio
1535
1536    def RequestPositions(self) -> dict:
1537        """
1538        Requesting open positions by currencies and instruments for current `accountId`.
1539
1540        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1541
1542        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1543
1544        :return: dictionary with open positions by instruments.
1545        """
1546        if self.accountId is None or not self.accountId:
1547            uLogger.error("Variable `accountId` must be defined for using this method!")
1548            raise Exception("Account ID required")
1549
1550        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1551
1552        self.body = str({"accountId": self.accountId})
1553        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1554        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1555
1556        if self.moreDebug:
1557            uLogger.debug("Records about current open positions successfully received")
1558
1559        return rawPositions
1560
1561    def RequestPendingOrders(self) -> list:
1562        """
1563        Requesting current actual pending limit orders for current `accountId`.
1564
1565        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1566
1567        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1568
1569        :return: list of dictionaries with pending limit orders.
1570        """
1571        if self.accountId is None or not self.accountId:
1572            uLogger.error("Variable `accountId` must be defined for using this method!")
1573            raise Exception("Account ID required")
1574
1575        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1576
1577        self.body = str({"accountId": self.accountId})
1578        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1579        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1580
1581        if "orders" in rawResponse.keys():
1582            rawOrders = rawResponse["orders"]
1583            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1584
1585        else:
1586            rawOrders = []
1587            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1588
1589        return rawOrders
1590
1591    def RequestStopOrders(self) -> list:
1592        """
1593        Requesting current actual stop orders for current `accountId`.
1594
1595        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1596
1597        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1598
1599        :return: list of dictionaries with stop orders.
1600        """
1601        if self.accountId is None or not self.accountId:
1602            uLogger.error("Variable `accountId` must be defined for using this method!")
1603            raise Exception("Account ID required")
1604
1605        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1606
1607        self.body = str({"accountId": self.accountId})
1608        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1609        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1610
1611        if "stopOrders" in rawResponse.keys():
1612            rawStopOrders = rawResponse["stopOrders"]
1613            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1614
1615        else:
1616            rawStopOrders = []
1617            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1618
1619        return rawStopOrders
1620
1621    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1622        """
1623        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1624        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1625        and `overviewBondsCalendarFile` are defined then also save information to file.
1626
1627        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1628        many requests about the state of the portfolio, and then, based on the received data, a large number
1629        of calculation and statistics are collected.
1630
1631        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1632        :param details: how detailed should the information be?
1633        - `full` — shows full available information about portfolio status (by default),
1634        - `positions` — shows only open positions,
1635        - `orders` — shows only sections of open limits and stop orders.
1636        - `digest` — show a short digest of the portfolio status,
1637        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1638        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1639        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1640        :return: dictionary with client's raw portfolio and some statistics.
1641        """
1642        if self.accountId is None or not self.accountId:
1643            uLogger.error("Variable `accountId` must be defined for using this method!")
1644            raise Exception("Account ID required")
1645
1646        view = {
1647            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1648                "headers": {},  # list of dictionaries, response headers without "positions" section
1649                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1650                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1651                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1652                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1653                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1654                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1655                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1656                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1657                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1658            },
1659            "stat": {  # --- some statistics calculated using "raw" sections:
1660                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1661                "availableRUB": 0.,  # available rubles (without other currencies)
1662                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1663                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1664                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1665                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1666                "sharesCostRUB": 0.,  # costs of all shares in RUB
1667                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1668                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1669                "futuresCostRUB": 0.,  # costs of all futures in RUB
1670                "Currencies": [],  # list of dictionaries of all currencies statistics
1671                "Shares": [],  # list of dictionaries of all shares statistics
1672                "Bonds": [],  # list of dictionaries of all bonds statistics
1673                "Etfs": [],  # list of dictionaries of all etfs statistics
1674                "Futures": [],  # list of dictionaries of all futures statistics
1675                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1676                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1677                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1678                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1679                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1680            },
1681            "analytics": {  # --- some analytics of portfolio:
1682                "distrByAssets": {},  # portfolio distribution by assets
1683                "distrByCompanies": {},  # portfolio distribution by companies
1684                "distrBySectors": {},  # portfolio distribution by sectors
1685                "distrByCurrencies": {},  # portfolio distribution by currencies
1686                "distrByCountries": {},  # portfolio distribution by countries
1687                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1688            }
1689        }
1690
1691        details = details.lower()
1692        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1693        if details not in availableDetails:
1694            details = "full"
1695            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1696
1697        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1698
1699        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1700        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1701        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1702        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1703
1704        # save response headers without "positions" section:
1705        for key in portfolioResponse.keys():
1706            if key != "positions":
1707                view["raw"]["headers"][key] = portfolioResponse[key]
1708
1709            else:
1710                continue
1711
1712        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1713        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1714        for item in portfolioResponse["positions"]:
1715            if item["instrumentType"] == "currency":
1716                self._figi = item["figi"]
1717                if not self._figi and item["ticker"]:
1718                    self._ticker = item["ticker"]
1719                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1720
1721                curr = self.SearchByFIGI(requestPrice=False)
1722
1723                # current price of currency in RUB:
1724                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1725                    "name": curr["name"],
1726                    "currentPrice": NanoToFloat(
1727                        item["currentPrice"]["units"],
1728                        item["currentPrice"]["nano"]
1729                    ),
1730                }
1731
1732                view["raw"]["Currencies"].append(item)
1733
1734            elif item["instrumentType"] == "share":
1735                view["raw"]["Shares"].append(item)
1736
1737            elif item["instrumentType"] == "bond":
1738                view["raw"]["Bonds"].append(item)
1739
1740            elif item["instrumentType"] == "etf":
1741                view["raw"]["Etfs"].append(item)
1742
1743            elif item["instrumentType"] == "futures":
1744                view["raw"]["Futures"].append(item)
1745
1746            else:
1747                continue
1748
1749        # how many volume of currencies (by ISO currency name) are blocked:
1750        for item in view["raw"]["positions"]["blocked"]:
1751            blocked = NanoToFloat(item["units"], item["nano"])
1752            if blocked > 0:
1753                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1754
1755        # how many volume of instruments (by FIGI) are blocked:
1756        for item in view["raw"]["positions"]["securities"]:
1757            blocked = int(item["blocked"])
1758            if blocked > 0:
1759                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1760
1761        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1762
1763        if "rub" in allBlocked.keys():
1764            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1765
1766        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1767        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1768        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1769        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1770        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1771        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1772        view["stat"]["portfolioCostRUB"] = sum([
1773            view["stat"]["allCurrenciesCostRUB"],
1774            view["stat"]["sharesCostRUB"],
1775            view["stat"]["bondsCostRUB"],
1776            view["stat"]["etfsCostRUB"],
1777            view["stat"]["futuresCostRUB"],
1778        ])
1779
1780        # --- calculating some portfolio statistics:
1781        byComp = {}  # distribution by companies
1782        bySect = {}  # distribution by sectors
1783        byCurr = {}  # distribution by currencies (include RUB)
1784        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1785        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1786
1787        for item in portfolioResponse["positions"]:
1788            self._figi = item["figi"]
1789            if not self._figi and item["ticker"]:
1790                self._ticker = item["ticker"]
1791                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1792
1793            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1794
1795            if instrument:
1796                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1797                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1798
1799                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1800                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1801
1802                else:
1803                    blocked = 0
1804
1805                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1806                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1807                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1808                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1809                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1810                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1811                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1812                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1813                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1814                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1815                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1816                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1817
1818                statData = {
1819                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1820                    "ticker": instrument["ticker"],  # ticker by FIGI
1821                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1822                    "volume": volume,  # available volume of instrument
1823                    "lots": lots,  # volume in lots of instrument
1824                    "direction": direction,  # direction of an instrument's position: short or long
1825                    "blocked": blocked,  # blocked volume of currency or instrument
1826                    "currentPrice": curPrice,  # current instrument's price in basic asset
1827                    "average": average,  # current average position price
1828                    "cost": cost,  # current cost of all volume of instrument in basic asset
1829                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1830                    "costRUB": costRUB,  # cost of instrument in ruble
1831                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1832                    "profit": profit,  # expected profit at current moment
1833                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1834                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1835                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1836                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1837                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1838                    "step": instrument["step"],  # minimum price increment
1839                }
1840
1841                # adding distribution by unique countries:
1842                if statData["country"] not in byCountry.keys():
1843                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1844
1845                else:
1846                    byCountry[statData["country"]]["cost"] += costRUB
1847                    byCountry[statData["country"]]["percent"] += percentCostRUB
1848
1849                if item["instrumentType"] != "currency":
1850                    # adding distribution by unique companies:
1851                    if statData["name"]:
1852                        if statData["name"] not in byComp.keys():
1853                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1854
1855                        else:
1856                            byComp[statData["name"]]["cost"] += costRUB
1857                            byComp[statData["name"]]["percent"] += percentCostRUB
1858
1859                    # adding distribution by unique sectors:
1860                    if statData["sector"] not in bySect.keys():
1861                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1862
1863                    else:
1864                        bySect[statData["sector"]]["cost"] += costRUB
1865                        bySect[statData["sector"]]["percent"] += percentCostRUB
1866
1867                # adding distribution by unique currencies:
1868                if currency not in byCurr.keys():
1869                    byCurr[currency] = {
1870                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1871                        "cost": costRUB,
1872                        "percent": percentCostRUB
1873                    }
1874
1875                else:
1876                    byCurr[currency]["cost"] += costRUB
1877                    byCurr[currency]["percent"] += percentCostRUB
1878
1879                # saving statistics for every instrument:
1880                if item["instrumentType"] == "currency":
1881                    view["stat"]["Currencies"].append(statData)
1882
1883                    # update dict with free funds for trading (total - blocked) by currencies
1884                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1885                    view["stat"]["funds"][currency] = {
1886                        "total": volume,
1887                        "totalCostRUB": costRUB,  # total volume cost in rubles
1888                        "free": volume - blocked,
1889                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1890                    }
1891
1892                elif item["instrumentType"] == "share":
1893                    view["stat"]["Shares"].append(statData)
1894
1895                elif item["instrumentType"] == "bond":
1896                    view["stat"]["Bonds"].append(statData)
1897
1898                elif item["instrumentType"] == "etf":
1899                    view["stat"]["Etfs"].append(statData)
1900
1901                elif item["instrumentType"] == "Futures":
1902                    view["stat"]["Futures"].append(statData)
1903
1904                else:
1905                    continue
1906
1907        # total changes in Russian Ruble:
1908        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1909        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1910        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1911        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1912        view["stat"]["funds"]["rub"] = {
1913            "total": view["stat"]["availableRUB"],
1914            "totalCostRUB": view["stat"]["availableRUB"],
1915            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1916            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1917        }
1918
1919        # --- pending limit orders sector data:
1920        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1921        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1922
1923        for item in view["raw"]["orders"]:
1924            self._figi = item["figi"]
1925
1926            if item["figi"] not in uniquePendingOrdersFIGIs:
1927                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1928
1929                uniquePendingOrdersFIGIs.append(item["figi"])
1930                uniquePendingOrders[item["figi"]] = instrument
1931
1932            else:
1933                instrument = uniquePendingOrders[item["figi"]]
1934
1935            if instrument:
1936                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1937                orderType = TKS_ORDER_TYPES[item["orderType"]]
1938                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1939                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1940
1941                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1942                if item["direction"] == "ORDER_DIRECTION_BUY":
1943                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1944
1945                else:
1946                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1947
1948                # requested price for order execution:
1949                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1950
1951                # necessary changes in percent to reach target from current price:
1952                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1953
1954                view["stat"]["orders"].append({
1955                    "orderID": item["orderId"],  # orderId number parameter of current order
1956                    "figi": item["figi"],  # FIGI identification
1957                    "ticker": instrument["ticker"],  # ticker name by FIGI
1958                    "lotsRequested": item["lotsRequested"],  # requested lots value
1959                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1960                    "currentPrice": lastPrice,  # current instrument's price for defined action
1961                    "targetPrice": target,  # requested price for order execution in base currency
1962                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1963                    "percentChanges": changes,  # changes in percent to target from current price
1964                    "currency": item["currency"],  # instrument's currency name
1965                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1966                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1967                    "status": orderState,  # order status from TKS_ORDER_STATES
1968                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1969                })
1970
1971        # --- stop orders sector data:
1972        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1973        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1974
1975        for item in view["raw"]["stopOrders"]:
1976            self._figi = item["figi"]
1977
1978            if item["figi"] not in uniqueStopOrdersFIGIs:
1979                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1980
1981                uniqueStopOrdersFIGIs.append(item["figi"])
1982                uniqueStopOrders[item["figi"]] = instrument
1983
1984            else:
1985                instrument = uniqueStopOrders[item["figi"]]
1986
1987            if instrument:
1988                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1989                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1990                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1991
1992                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1993                if "expirationTime" in item.keys():
1994                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1995                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1996
1997                else:
1998                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1999                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2000
2001                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2002                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2003                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2004
2005                else:
2006                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2007
2008                # requested price when stop-order executed:
2009                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2010
2011                # price for limit-order, set up when stop-order executed:
2012                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2013
2014                # necessary changes in percent to reach target from current price:
2015                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2016
2017                view["stat"]["stopOrders"].append({
2018                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2019                    "figi": item["figi"],  # FIGI identification
2020                    "ticker": instrument["ticker"],  # ticker name by FIGI
2021                    "lotsRequested": item["lotsRequested"],  # requested lots value
2022                    "currentPrice": lastPrice,  # current instrument's price for defined action
2023                    "targetPrice": target,  # requested price for stop-order execution in base currency
2024                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2025                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2026                    "percentChanges": changes,  # changes in percent to target from current price
2027                    "currency": item["currency"],  # instrument's currency name
2028                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2029                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2030                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2031                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2032                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2033                })
2034
2035        # --- calculating data for analytics section:
2036        # portfolio distribution by assets:
2037        view["analytics"]["distrByAssets"] = {
2038            "Ruble": {
2039                "uniques": 1,
2040                "cost": view["stat"]["availableRUB"],
2041                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2042            },
2043            "Currencies": {
2044                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2045                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2046                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2047            },
2048            "Shares": {
2049                "uniques": len(view["stat"]["Shares"]),
2050                "cost": view["stat"]["sharesCostRUB"],
2051                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2052            },
2053            "Bonds": {
2054                "uniques": len(view["stat"]["Bonds"]),
2055                "cost": view["stat"]["bondsCostRUB"],
2056                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2057            },
2058            "Etfs": {
2059                "uniques": len(view["stat"]["Etfs"]),
2060                "cost": view["stat"]["etfsCostRUB"],
2061                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2062            },
2063            "Futures": {
2064                "uniques": len(view["stat"]["Futures"]),
2065                "cost": view["stat"]["futuresCostRUB"],
2066                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2067            },
2068        }
2069
2070        # portfolio distribution by companies:
2071        view["analytics"]["distrByCompanies"]["All money cash"] = {
2072            "ticker": "",
2073            "cost": view["stat"]["allCurrenciesCostRUB"],
2074            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2075        }
2076        view["analytics"]["distrByCompanies"].update(byComp)
2077
2078        # portfolio distribution by sectors:
2079        view["analytics"]["distrBySectors"]["All money cash"] = {
2080            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2081            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2082        }
2083        view["analytics"]["distrBySectors"].update(bySect)
2084
2085        # portfolio distribution by currencies:
2086        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2087            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2088
2089            if self.moreDebug:
2090                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2091
2092        view["analytics"]["distrByCurrencies"].update(byCurr)
2093        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2094        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2095
2096        # portfolio distribution by countries:
2097        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2098            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2099
2100            if self.moreDebug:
2101                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2102
2103        view["analytics"]["distrByCountries"].update(byCountry)
2104        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2105        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2106
2107        # --- Prepare text statistics overview in human-readable:
2108        if show or onlyFiles:
2109            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2110
2111            # Whatever the value `details`, header not changes:
2112            info = [
2113                "# Client's portfolio\n\n",
2114                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2115                "* **Account ID:** [{}]\n".format(self.accountId),
2116            ]
2117
2118            if details in ["full", "positions", "digest"]:
2119                info.extend([
2120                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2121                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2122                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2123                        view["stat"]["totalChangesRUB"],
2124                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2125                        view["stat"]["totalChangesPercentRUB"],
2126                    ),
2127                ])
2128
2129            if details in ["full", "positions"]:
2130                info.extend([
2131                    "## Open positions\n\n",
2132                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2133                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2134                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2135                        "{:.2f} ({:.2f}) rub".format(
2136                            view["stat"]["availableRUB"],
2137                            view["stat"]["blockedRUB"],
2138                        )
2139                    )
2140                ])
2141
2142                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2143                    return [
2144                        "|                             |                                 |          |              |              |                     |                              |\n",
2145                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2146                            noTradeStr if noTradeStr else typeStr,
2147                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2148                        ),
2149                    ]
2150
2151                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2152                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2153                        "{} [{}]".format(data["ticker"], data["figi"]),
2154                        "{:.2f} ({:.2f}) {}".format(
2155                            data["volume"],
2156                            data["blocked"],
2157                            data["currency"],
2158                        ) if isCurr else "{:.0f} ({:.0f})".format(
2159                            data["volume"],
2160                            data["blocked"],
2161                        ),
2162                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2163                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2164                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2165                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2166                        "{}{:.2f} {} ({}{:.2f}%)".format(
2167                            "+" if data["profit"] > 0 else "",
2168                            data["profit"], data["baseCurrencyName"],
2169                            "+" if data["percentProfit"] > 0 else "",
2170                            data["percentProfit"],
2171                        ),
2172                    )
2173
2174                # --- Show currencies section:
2175                if view["stat"]["Currencies"]:
2176                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2177                    for item in view["stat"]["Currencies"]:
2178                        info.append(_InfoStr(item, isCurr=True))
2179
2180                else:
2181                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2182
2183                # --- Show shares section:
2184                if view["stat"]["Shares"]:
2185                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2186
2187                    for item in view["stat"]["Shares"]:
2188                        info.append(_InfoStr(item))
2189
2190                else:
2191                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2192
2193                # --- Show bonds section:
2194                if view["stat"]["Bonds"]:
2195                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2196
2197                    for item in view["stat"]["Bonds"]:
2198                        info.append(_InfoStr(item))
2199
2200                else:
2201                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2202
2203                # --- Show etfs section:
2204                if view["stat"]["Etfs"]:
2205                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2206
2207                    for item in view["stat"]["Etfs"]:
2208                        info.append(_InfoStr(item))
2209
2210                else:
2211                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2212
2213                # --- Show futures section:
2214                if view["stat"]["Futures"]:
2215                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2216
2217                    for item in view["stat"]["Futures"]:
2218                        info.append(_InfoStr(item))
2219
2220                else:
2221                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2222
2223            if details in ["full", "orders"]:
2224                # --- Show pending limit orders section:
2225                if view["stat"]["orders"]:
2226                    info.extend([
2227                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2228                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2229                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2230                    ])
2231
2232                    for item in view["stat"]["orders"]:
2233                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2234                            "{} [{}]".format(item["ticker"], item["figi"]),
2235                            item["orderID"],
2236                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2237                            "{} {} ({}{:.2f}%)".format(
2238                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2239                                item["baseCurrencyName"],
2240                                "+" if item["percentChanges"] > 0 else "",
2241                                float(item["percentChanges"]),
2242                            ),
2243                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2244                            item["action"],
2245                            item["type"],
2246                            item["date"],
2247                        ))
2248
2249                else:
2250                    info.append("\n## Total pending limit-orders: [0]\n")
2251
2252                # --- Show stop orders section:
2253                if view["stat"]["stopOrders"]:
2254                    info.extend([
2255                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2256                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2257                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2258                    ])
2259
2260                    for item in view["stat"]["stopOrders"]:
2261                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2262                            "{} [{}]".format(item["ticker"], item["figi"]),
2263                            item["orderID"],
2264                            item["lotsRequested"],
2265                            "{} {} ({}{:.2f}%)".format(
2266                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2267                                item["baseCurrencyName"],
2268                                "+" if item["percentChanges"] > 0 else "",
2269                                float(item["percentChanges"]),
2270                            ),
2271                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2272                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2273                            item["action"],
2274                            item["type"],
2275                            item["expType"],
2276                            item["createDate"],
2277                            item["expDate"],
2278                        ))
2279
2280                else:
2281                    info.append("\n## Total stop-orders: [0]\n")
2282
2283            if details in ["full", "analytics"]:
2284                # -- Show analytics section:
2285                if view["stat"]["portfolioCostRUB"] > 0:
2286                    info.extend([
2287                        "\n# Analytics\n\n"
2288                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2289                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2290                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2291                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2292                            view["stat"]["totalChangesRUB"],
2293                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2294                            view["stat"]["totalChangesPercentRUB"],
2295                        ),
2296                        "\n## Portfolio distribution by assets\n"
2297                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2298                        "|------------------------------------|---------|---------|--------------------|\n",
2299                    ])
2300
2301                    for key in view["analytics"]["distrByAssets"].keys():
2302                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2303                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2304                                key,
2305                                view["analytics"]["distrByAssets"][key]["uniques"],
2306                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2307                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2308                            ))
2309
2310                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2311
2312                    info.extend([
2313                        "\n## Portfolio distribution by companies\n"
2314                        "\n| Company                                      | Percent | Current cost       |\n",
2315                        aSepLine,
2316                    ])
2317
2318                    for company in view["analytics"]["distrByCompanies"].keys():
2319                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2320                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2321                                "{}{}".format(
2322                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2323                                    company,
2324                                ),
2325                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2326                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2327                            ))
2328
2329                    info.extend([
2330                        "\n## Portfolio distribution by sectors\n"
2331                        "\n| Sector                                       | Percent | Current cost       |\n",
2332                        aSepLine,
2333                    ])
2334
2335                    for sector in view["analytics"]["distrBySectors"].keys():
2336                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2337                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2338                                sector,
2339                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2340                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2341                            ))
2342
2343                    info.extend([
2344                        "\n## Portfolio distribution by currencies\n"
2345                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2346                        aSepLine,
2347                    ])
2348
2349                    for curr in view["analytics"]["distrByCurrencies"].keys():
2350                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2351                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2352                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2353                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2354                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2355                            ))
2356
2357                    info.extend([
2358                        "\n## Portfolio distribution by countries\n"
2359                        "\n| Assets by country                            | Percent | Current cost       |\n",
2360                        aSepLine,
2361                    ])
2362
2363                    for country in view["analytics"]["distrByCountries"].keys():
2364                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2365                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2366                                country,
2367                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2368                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2369                            ))
2370
2371            if details in ["full", "calendar"]:
2372                # -- Show bonds payment calendar section:
2373                if view["stat"]["Bonds"]:
2374                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2375                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2376                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2377
2378                else:
2379                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2380
2381            infoText = "".join(info)
2382
2383            if show and not onlyFiles:
2384                uLogger.info(infoText)
2385
2386            if details == "full" and self.overviewFile:
2387                filename = self.overviewFile
2388
2389            elif details == "digest" and self.overviewDigestFile:
2390                filename = self.overviewDigestFile
2391
2392            elif details == "positions" and self.overviewPositionsFile:
2393                filename = self.overviewPositionsFile
2394
2395            elif details == "orders" and self.overviewOrdersFile:
2396                filename = self.overviewOrdersFile
2397
2398            elif details == "analytics" and self.overviewAnalyticsFile:
2399                filename = self.overviewAnalyticsFile
2400
2401            elif details == "calendar" and self.overviewBondsCalendarFile:
2402                filename = self.overviewBondsCalendarFile
2403
2404            else:
2405                filename = ""
2406
2407            if filename and (show or onlyFiles):
2408                with open(filename, "w", encoding="UTF-8") as fH:
2409                    fH.write(infoText)
2410
2411                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2412
2413                if self.useHTMLReports:
2414                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2415                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2416                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2417
2418                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2419
2420        return view
2421
2422    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2423        """
2424        Returns history operations between two given dates for current `accountId`.
2425        If `reportFile` string is not empty then also save human-readable report.
2426        Shows some statistical data of closed positions.
2427
2428        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2429        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2430        :param show: if `True` then also prints all records to the console.
2431        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2432        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2433        :return: original list of dictionaries with history of deals records from API ("operations" key):
2434                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2435                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2436        """
2437        if self.accountId is None or not self.accountId:
2438            uLogger.error("Variable `accountId` must be defined for using this method!")
2439            raise Exception("Account ID required")
2440
2441        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2442
2443        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2444
2445        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2446        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2447        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2448        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2449        customStat = {}  # custom statistics in additional to responseJSON
2450
2451        # --- output report in human-readable format:
2452        if self.reportFile and (show or onlyFiles):
2453            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2454            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2455            nextDay = ""
2456
2457            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2458
2459            if len(ops) > 0:
2460                customStat = {
2461                    "opsCount": 0,  # total operations count
2462                    "buyCount": 0,  # buy operations
2463                    "sellCount": 0,  # sell operations
2464                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2465                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2466                    "payIn": {"rub": 0.},  # Deposit brokerage account
2467                    "payOut": {"rub": 0.},  # Withdrawals
2468                    "divs": {"rub": 0.},  # Dividends income
2469                    "coupons": {"rub": 0.},  # Coupon's income
2470                    "brokerCom": {"rub": 0.},  # Service commissions
2471                    "serviceCom": {"rub": 0.},  # Service commissions
2472                    "marginCom": {"rub": 0.},  # Margin commissions
2473                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2474                }
2475
2476                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2477                for item in ops:
2478                    if item["state"] == "OPERATION_STATE_EXECUTED":
2479                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2480
2481                        # count buy operations:
2482                        if "_BUY" in item["operationType"]:
2483                            customStat["buyCount"] += 1
2484
2485                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2486                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2487
2488                            else:
2489                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2490
2491                        # count sell operations:
2492                        elif "_SELL" in item["operationType"]:
2493                            customStat["sellCount"] += 1
2494
2495                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2496                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2497
2498                            else:
2499                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2500
2501                        # count incoming operations:
2502                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2503                            if item["payment"]["currency"] in customStat["payIn"].keys():
2504                                customStat["payIn"][item["payment"]["currency"]] += payment
2505
2506                            else:
2507                                customStat["payIn"][item["payment"]["currency"]] = payment
2508
2509                        # count withdrawals operations:
2510                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2511                            if item["payment"]["currency"] in customStat["payOut"].keys():
2512                                customStat["payOut"][item["payment"]["currency"]] += payment
2513
2514                            else:
2515                                customStat["payOut"][item["payment"]["currency"]] = payment
2516
2517                        # count dividends income:
2518                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2519                            if item["payment"]["currency"] in customStat["divs"].keys():
2520                                customStat["divs"][item["payment"]["currency"]] += payment
2521
2522                            else:
2523                                customStat["divs"][item["payment"]["currency"]] = payment
2524
2525                        # count coupon's income:
2526                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2527                            if item["payment"]["currency"] in customStat["coupons"].keys():
2528                                customStat["coupons"][item["payment"]["currency"]] += payment
2529
2530                            else:
2531                                customStat["coupons"][item["payment"]["currency"]] = payment
2532
2533                        # count broker commissions:
2534                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2535                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2536                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2537
2538                            else:
2539                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2540
2541                        # count service commissions:
2542                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2543                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2544                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2545
2546                            else:
2547                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2548
2549                        # count margin commissions:
2550                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2551                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2552                                customStat["marginCom"][item["payment"]["currency"]] += payment
2553
2554                            else:
2555                                customStat["marginCom"][item["payment"]["currency"]] = payment
2556
2557                        # count withholding taxes:
2558                        elif "_TAX" in item["operationType"]:
2559                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2560                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2561
2562                            else:
2563                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2564
2565                        else:
2566                            continue
2567
2568                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2569
2570                # --- view "Actions" lines:
2571                info.extend([
2572                    "| Report sections            |                               |                              |                      |                        |\n",
2573                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2574                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2575                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2576                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2577                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2578                    ),
2579                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2580                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2581                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2582                    ),
2583                ])
2584
2585                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2586                for key in opsKeys:
2587                    if key == "rub":
2588                        continue
2589
2590                    info.extend([
2591                        "|                            |                               | {:<28} |                      |                        |\n".format(
2592                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2593                        ),
2594                        "|                            |                               | {:<28} |                      |                        |\n".format(
2595                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2596                        ),
2597                    ])
2598
2599                info.append(splitLine1)
2600
2601                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2602                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2603                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2604                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2605                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2606                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2607                    )
2608
2609                # --- view "Payments" lines:
2610                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2611                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2612
2613                for key in paymentsKeys:
2614                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2615
2616                info.append(splitLine1)
2617
2618                # --- view "Commissions and taxes" lines:
2619                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2620                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2621
2622                for key in comKeys:
2623                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2624
2625                info.extend([
2626                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2627                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2628                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2629                ])
2630
2631            else:
2632                info.append("Broker returned no operations during this period\n")
2633
2634            # --- view "Operations" section:
2635            for item in ops:
2636                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2637                    continue
2638
2639                else:
2640                    self._figi = item["figi"]
2641                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2642                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2643
2644                    # group of deals during one day:
2645                    if nextDay and item["date"].split("T")[0] != nextDay:
2646                        info.append(splitLine2)
2647                        nextDay = ""
2648
2649                    else:
2650                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2651
2652                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2653                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2654                        self._figi if self._figi else "—",
2655                        instrument["ticker"] if instrument else "—",
2656                        instrument["type"] if instrument else "—",
2657                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2658                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2659                        TKS_OPERATION_STATES[item["state"]],
2660                        TKS_OPERATION_TYPES[item["operationType"]],
2661                    ))
2662
2663            infoText = "".join(info)
2664
2665            if show and not onlyFiles:
2666                if self.moreDebug:
2667                    uLogger.debug("Records about history of a client's operations successfully received")
2668
2669                uLogger.info(infoText)
2670
2671            if self.reportFile and (show or onlyFiles):
2672                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2673                    fH.write(infoText)
2674
2675                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2676
2677                if self.useHTMLReports:
2678                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2679                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2680                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2681
2682                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2683
2684        return ops, customStat
2685
2686    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2687        """
2688        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2689
2690        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2691        Warning! Broker server used ISO UTC time by default.
2692
2693        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2694        Also, `historyFile` used to update history with `onlyMissing` parameter.
2695
2696        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2697
2698        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2699        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2700        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2701                         `"hour"`, `"day"`. Default: `"hour"`.
2702        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2703                            False by default. Warning! History appends only from last candle to current time
2704                            with always update last candle!
2705        :param csvSep: separator if csv-file is used, `,` by default.
2706        :param show: if `True` then also prints Pandas DataFrame to the console.
2707        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2708        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2709                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2710        """
2711        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2712        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2713        history = None  # empty pandas object for history
2714
2715        if interval not in TKS_CANDLE_INTERVALS.keys():
2716            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2717            raise Exception("Incorrect value")
2718
2719        if not (self._ticker or self._figi):
2720            uLogger.error("Ticker or FIGI must be defined!")
2721            raise Exception("Ticker or FIGI required")
2722
2723        if self._ticker and not self._figi:
2724            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2725            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2726
2727        if self._figi and not self._ticker:
2728            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2729            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2730
2731        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2732        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2733        if interval.lower() != "day":
2734            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2735
2736        delta = dtEnd - dtStart  # current UTC time minus last time in file
2737        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2738
2739        # calculate history length in candles:
2740        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2741        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2742            length += 1  # to avoid fraction time
2743
2744        # calculate data blocks count:
2745        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2746
2747        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2748        if self.moreDebug:
2749            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2750            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2751            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2752            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2753
2754        tempOld = None  # pandas object for old history, if --only-missing key present
2755        lastTime = None  # datetime object of last old candle in file
2756
2757        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2758            if self.moreDebug:
2759                uLogger.debug("--only-missing key present, add only last missing candles...")
2760                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2761
2762            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2763
2764            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2765            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2766            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2767            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2768
2769            # get last datetime object from last string in file or minus 1 delta if file is empty:
2770            if len(tempOld) > 0:
2771                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2772
2773            else:
2774                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2775
2776            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2777
2778        responseJSONs = []  # raw history blocks of data
2779
2780        blockEnd = dtEnd
2781        for item in range(blocks):
2782            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2783            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2784
2785            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2786                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2787            ))
2788
2789            if blockStart == blockEnd:
2790                uLogger.debug("Skipped this zero-length block...")
2791
2792            else:
2793                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2794                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2795                self.body = str({
2796                    "figi": self._figi,
2797                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2798                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2799                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2800                })
2801                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2802
2803                if "code" in responseJSON.keys():
2804                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2805
2806                else:
2807                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2808                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2809
2810                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2811
2812            blockEnd = blockStart
2813
2814        printCount = len(responseJSONs)  # candles to show in console
2815        if responseJSONs:
2816            tempHistory = pd.DataFrame(
2817                data={
2818                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2819                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2820                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2821                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2822                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2823                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2824                    "volume": [int(item["volume"]) for item in responseJSONs],
2825                },
2826                index=range(len(responseJSONs)),
2827                columns=["date", "time", "open", "high", "low", "close", "volume"],
2828            )
2829            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2830            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2831
2832            # append only newest candles to old history if --only-missing key present:
2833            if onlyMissing and tempOld is not None and lastTime is not None:
2834                index = 0  # find start index in tempHistory data:
2835
2836                for i, item in tempHistory.iterrows():
2837                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2838
2839                    if curTime == lastTime:
2840                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2841                        index = i
2842                        printCount = index + 1
2843                        break
2844
2845                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2846
2847            else:
2848                history = tempHistory  # if no `--only-missing` key then load full data from server
2849
2850            if self.moreDebug:
2851                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2852
2853        if history is not None and not history.empty:
2854            if show and not onlyFiles:
2855                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2856                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2857                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2858                ))
2859
2860        else:
2861            uLogger.warning("Received an empty candles history!")
2862
2863        if self.historyFile is not None:
2864            if history is not None and not history.empty:
2865                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2866                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2867
2868            else:
2869                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2870
2871        else:
2872            if self.moreDebug:
2873                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2874
2875        return history
2876
2877    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2878        """
2879        Load candles history from csv-file and return Pandas DataFrame object.
2880
2881        See also: `History()` and `ShowHistoryChart()` methods.
2882
2883        :param filePath: path to csv-file to open.
2884        """
2885        loadedHistory = None  # init candles data object
2886
2887        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2888
2889        if os.path.exists(filePath):
2890            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2891
2892            tfStr = self.priceModel.FormattedDelta(
2893                self.priceModel.timeframe,
2894                "{days} days {hours}h {minutes}m {seconds}s",
2895            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2896                self.priceModel.timeframe,
2897                "{hours}h {minutes}m {seconds}s",
2898            )
2899
2900            if loadedHistory is not None and not loadedHistory.empty:
2901                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2902                    len(loadedHistory),
2903                    tfStr,
2904                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2905                )
2906
2907            else:
2908                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2909
2910        else:
2911            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2912
2913        return loadedHistory
2914
2915    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2916        """
2917        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2918
2919        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2920        Default: `index.html` (both for interact and non-interact candlesticks chart).
2921
2922        See also: `History()` and `LoadHistory()` methods.
2923
2924        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2925        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2926                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2927                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2928                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2929        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2930                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2931        """
2932        if isinstance(candles, str):
2933            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2934            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2935
2936        elif isinstance(candles, pd.DataFrame):
2937            self.priceModel.prices = candles  # set candles chain from variable
2938            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2939
2940            if "datetime" not in candles.columns:
2941                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2942
2943        else:
2944            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2945            raise Exception("Incorrect value")
2946
2947        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2948
2949        if interact:
2950            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2951
2952            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2953
2954        else:
2955            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2956
2957            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2958
2959        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2960
2961    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2962        """
2963        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2964        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2965
2966        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2967
2968        :param operation: string "Buy" or "Sell".
2969        :param lots: volume, integer count of lots >= 1.
2970        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2971        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2972        :param expDate: string "Undefined" by default or local date in future,
2973                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2974        :return: JSON with response from broker server.
2975        """
2976        if self.accountId is None or not self.accountId:
2977            uLogger.error("Variable `accountId` must be defined for using this method!")
2978            raise Exception("Account ID required")
2979
2980        if operation is None or not operation or operation not in ("Buy", "Sell"):
2981            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2982            raise Exception("Incorrect value")
2983
2984        if lots is None or lots < 1:
2985            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2986            lots = 1
2987
2988        if tp is None or tp < 0:
2989            tp = 0
2990
2991        if sl is None or sl < 0:
2992            sl = 0
2993
2994        if expDate is None or not expDate:
2995            expDate = "Undefined"
2996
2997        if not (self._ticker or self._figi):
2998            uLogger.error("Ticker or FIGI must be defined!")
2999            raise Exception("Ticker or FIGI required")
3000
3001        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3002        self._ticker = instrument["ticker"]
3003        self._figi = instrument["figi"]
3004
3005        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3006
3007        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3008        self.body = str({
3009            "figi": self._figi,
3010            "quantity": str(lots),
3011            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3012            "accountId": str(self.accountId),
3013            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3014        })
3015        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3016
3017        if "orderId" in response.keys():
3018            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3019                operation, response["orderId"],
3020                self._ticker, self._figi, lots,
3021                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3022                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3023                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3024            ))
3025
3026            if tp > 0:
3027                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3028
3029            if sl > 0:
3030                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3031
3032        else:
3033            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3034
3035        return response
3036
3037    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3038        """
3039        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3040        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3041
3042        See also: `Order()` and `Trade()` docstrings.
3043
3044        :param lots: volume, integer count of lots >= 1.
3045        :param tp: float > 0, take profit price of stop-order.
3046        :param sl: float > 0, stop loss price of stop-order.
3047        :param expDate: it's a local date in future.
3048                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3049        :return: JSON with response from broker server.
3050        """
3051        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3052
3053    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3054        """
3055        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3056        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3057
3058        See also: `Order()` and `Trade()` docstrings.
3059
3060        :param lots: volume, integer count of lots >= 1.
3061        :param tp: float > 0, take profit price of stop-order.
3062        :param sl: float > 0, stop loss price of stop-order.
3063        :param expDate: it's a local date in the future.
3064                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3065        :return: JSON with response from broker server.
3066        """
3067        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3068
3069    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3070        """
3071        Close position of given instruments.
3072
3073        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3074        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3075                         This avoids unnecessary downloading data from the server.
3076        """
3077        if instruments is None or not instruments:
3078            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3079            raise Exception("Ticker or FIGI required")
3080
3081        if isinstance(instruments, str):
3082            instruments = [instruments]
3083
3084        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3085        if uniqueInstruments:
3086            if portfolio is None or not portfolio:
3087                portfolio = self.Overview(show=False)
3088
3089            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3090            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3091
3092            for self._figi in uniqueInstruments:
3093                if self._figi not in allOpened:
3094                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3095                    continue
3096
3097                # search open trade info about instrument by ticker:
3098                instrument = {}
3099                for iType in TKS_INSTRUMENTS:
3100                    if instrument:
3101                        break
3102
3103                    for item in portfolio["stat"][iType]:
3104                        if item["figi"] == self._figi:
3105                            instrument = item
3106                            break
3107
3108                if instrument:
3109                    self._ticker = instrument["ticker"]
3110                    self._figi = instrument["figi"]
3111
3112                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3113                        self._ticker,
3114                        self._figi,
3115                        int(instrument["volume"]),
3116                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3117                    ))
3118
3119                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3120
3121                    if tradeLots > 0:
3122                        if instrument["blocked"] > 0:
3123                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3124                                instrument["blocked"],
3125                                self._ticker,
3126                                tradeLots,
3127                            ))
3128
3129                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3130                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3131
3132                    else:
3133                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3134
3135    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3136        """
3137        Close all positions of given instruments with defined type.
3138
3139        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3140        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3141                         This avoids unnecessary downloading data from the server.
3142        """
3143        if iType not in TKS_INSTRUMENTS:
3144            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3145
3146        else:
3147            if portfolio is None or not portfolio:
3148                portfolio = self.Overview(show=False)
3149
3150            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3151            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3152
3153            if tickers and portfolio:
3154                self.CloseTrades(tickers, portfolio)
3155
3156            else:
3157                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3158
3159    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3160        """
3161        Universal method to create market or limit orders with all available parameters for current `accountId`.
3162        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3163
3164        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3165        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3166
3167        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3168        then broker immediately open market order as you can do simple --buy or --sell operations!
3169
3170        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3171        When current price will go up or down to target price value then broker opens a limit order.
3172        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3173
3174        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3175
3176        :param operation: string "Buy" or "Sell".
3177        :param orderType: string "Limit" or "Stop".
3178        :param lots: volume, integer count of lots >= 1.
3179        :param targetPrice: target price > 0. This is open trade price for limit order.
3180        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3181                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3182        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3183                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3184                         Stop loss order always executed by market price.
3185        :param expDate: string "Undefined" by default or local date in future.
3186                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3187                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3188                        A limit order has no expiration date, it lasts until the end of the trading day.
3189        :return: JSON with response from broker server.
3190        """
3191        if self.accountId is None or not self.accountId:
3192            uLogger.error("Variable `accountId` must be defined for using this method!")
3193            raise Exception("Account ID required")
3194
3195        if operation is None or not operation or operation not in ("Buy", "Sell"):
3196            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3197            raise Exception("Incorrect value")
3198
3199        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3200            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3201            raise Exception("Incorrect value")
3202
3203        if lots is None or lots < 1:
3204            uLogger.error("You must define trade volume > 0: integer count of lots!")
3205            raise Exception("Incorrect value")
3206
3207        if targetPrice is None or targetPrice <= 0:
3208            uLogger.error("Target price for limit-order must be greater than 0!")
3209            raise Exception("Incorrect value")
3210
3211        if limitPrice is None or limitPrice <= 0:
3212            limitPrice = targetPrice
3213
3214        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3215            stopType = "Limit"
3216
3217        if expDate is None or not expDate:
3218            expDate = "Undefined"
3219
3220        if not (self._ticker or self._figi):
3221            uLogger.error("Tocker or FIGI must be defined!")
3222            raise Exception("Ticker or FIGI required")
3223
3224        response = {}
3225        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3226        self._ticker = instrument["ticker"]
3227        self._figi = instrument["figi"]
3228
3229        if orderType == "Limit":
3230            uLogger.debug(
3231                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3232                    self._ticker, self._figi,
3233                    operation, lots, targetPrice, instrument["currency"],
3234                ))
3235
3236            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3237            self.body = str({
3238                "figi": self._figi,
3239                "quantity": str(lots),
3240                "price": FloatToNano(targetPrice),
3241                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3242                "accountId": str(self.accountId),
3243                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3244            })
3245            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3246
3247            if "orderId" in response.keys():
3248                uLogger.info(
3249                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3250                        response["orderId"], self._ticker, self._figi, operation, lots,
3251                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3252                    ))
3253
3254                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3255                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3256                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3257                            targetPrice, instrument["currency"],
3258                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3259                        ))
3260
3261                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3262                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3263                            targetPrice, instrument["currency"],
3264                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3265                        ))
3266
3267            else:
3268                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3269
3270        if orderType == "Stop":
3271            uLogger.debug(
3272                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3273                    self._ticker, self._figi,
3274                    operation, lots,
3275                    targetPrice, instrument["currency"],
3276                    limitPrice, instrument["currency"],
3277                    stopType, expDate,
3278                ))
3279
3280            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3281            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3282            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3283
3284            body = {
3285                "figi": self._figi,
3286                "quantity": str(lots),
3287                "price": FloatToNano(limitPrice),
3288                "stopPrice": FloatToNano(targetPrice),
3289                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3290                "accountId": str(self.accountId),
3291                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3292                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3293            }
3294
3295            if expDateUTC:
3296                body["expireDate"] = expDateUTC
3297
3298            self.body = str(body)
3299            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3300
3301            if "stopOrderId" in response.keys():
3302                uLogger.info(
3303                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3304                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3305                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3306                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3307                        TKS_STOP_ORDER_TYPES[stopOrderType],
3308                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3309                    ))
3310
3311                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3312                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3313                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3314                            targetPrice, instrument["currency"],
3315                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3316                        ))
3317
3318                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3319                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3320                            targetPrice, instrument["currency"],
3321                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3322                        ))
3323
3324            else:
3325                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3326
3327        return response
3328
3329    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3330        """
3331        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3332        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3333        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3334        See also: `Order()` docstring.
3335
3336        :param lots: volume, integer count of lots >= 1.
3337        :param targetPrice: target price > 0. This is open trade price for limit order.
3338        :return: JSON with response from broker server.
3339        """
3340        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3341
3342    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3343        """
3344        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3345        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3346        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3347        target price value then broker opens a limit order. See also: `Order()` docstring.
3348
3349        :param lots: volume, integer count of lots >= 1.
3350        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3351        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3352                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3353        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3354                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3355        :param expDate: string "Undefined" by default or local date in future.
3356                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3357                        This date is converting to UTC format for server.
3358        :return: JSON with response from broker server.
3359        """
3360        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3361
3362    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3363        """
3364        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3365        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3366        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3367        See also: `Order()` docstring.
3368
3369        :param lots: volume, integer count of lots >= 1.
3370        :param targetPrice: target price > 0. This is open trade price for limit order.
3371        :return: JSON with response from broker server.
3372        """
3373        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3374
3375    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3376        """
3377        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3378        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3379        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3380        target price value then broker opens a limit order. See also: `Order()` docstring.
3381
3382        :param lots: volume, integer count of lots >= 1.
3383        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3384        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3385                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3386        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3387                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3388        :param expDate: string "Undefined" by default or local date in future.
3389                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3390                        This date is converting to UTC format for server.
3391        :return: JSON with response from broker server.
3392        """
3393        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3394
3395    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3396        """
3397        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3398
3399        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3400        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3401                             This avoids unnecessary downloading data from the server.
3402        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3403        """
3404        if self.accountId is None or not self.accountId:
3405            uLogger.error("Variable `accountId` must be defined for using this method!")
3406            raise Exception("Account ID required")
3407
3408        if orderIDs:
3409            if allOrdersIDs is None:
3410                rawOrders = self.RequestPendingOrders()
3411                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3412
3413            if allStopOrdersIDs is None:
3414                rawStopOrders = self.RequestStopOrders()
3415                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3416
3417            for orderID in orderIDs:
3418                idInPendingOrders = orderID in allOrdersIDs
3419                idInStopOrders = orderID in allStopOrdersIDs
3420
3421                if not (idInPendingOrders or idInStopOrders):
3422                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3423                    continue
3424
3425                else:
3426                    if idInPendingOrders:
3427                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3428
3429                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3430                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3431                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3432                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3433
3434                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3435                            if self.moreDebug:
3436                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3437
3438                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3439
3440                        else:
3441                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3442
3443                    elif idInStopOrders:
3444                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3445
3446                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3447                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3448                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3449                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3450
3451                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3452                            if self.moreDebug:
3453                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3454
3455                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3456
3457                        else:
3458                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3459
3460                    else:
3461                        continue
3462
3463    def CloseAllOrders(self) -> None:
3464        """
3465        Gets a list of open pending and stop orders and cancel it all.
3466        """
3467        rawOrders = self.RequestPendingOrders()
3468        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3469        lenOrders = len(allOrdersIDs)
3470
3471        rawStopOrders = self.RequestStopOrders()
3472        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3473        lenSOrders = len(allStopOrdersIDs)
3474
3475        if lenOrders > 0 or lenSOrders > 0:
3476            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3477
3478            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3479
3480        else:
3481            uLogger.info("Orders not found, nothing to cancel.")
3482
3483    def CloseAll(self, *args) -> None:
3484        """
3485        Close all available (not blocked) opened trades and orders.
3486
3487        Also, you can select one or more keywords case-insensitive:
3488        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3489
3490        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3491        """
3492        overview = self.Overview(show=False)  # get all open trades info
3493
3494        if len(args) == 0:
3495            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3496            self.CloseAllOrders()  # close all pending and stop orders
3497
3498            for iType in TKS_INSTRUMENTS:
3499                if iType != "Currencies":
3500                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3501
3502        else:
3503            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3504            lowerArgs = [x.lower() for x in args]
3505
3506            if "orders" in lowerArgs:
3507                self.CloseAllOrders()  # close all pending and stop orders
3508
3509            for iType in TKS_INSTRUMENTS:
3510                if iType.lower() in lowerArgs and iType != "Currencies":
3511                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3512
3513    def CloseAllByTicker(self, instrument: str) -> None:
3514        """
3515        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3516
3517        This method searches opened trade and orders of instrument throw all portfolio and then use
3518        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3519
3520        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3521
3522        :param instrument: string with ticker.
3523        """
3524        if instrument is None or not instrument:
3525            uLogger.error("Ticker name must be defined for using this method!")
3526            raise Exception("Ticker required")
3527
3528        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3529
3530        self._ticker = instrument  # try to set instrument as ticker
3531        self._figi = ""
3532
3533        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3534        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3535
3536        if limitAll and self.IsInLimitOrders(portfolio=overview):
3537            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3538            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3539
3540        if stopAll and self.IsInStopOrders(portfolio=overview):
3541            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3542            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3543
3544        if self.IsInPortfolio(portfolio=overview):
3545            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3546            self.CloseTrades(instruments=[instrument], portfolio=overview)
3547
3548    def CloseAllByFIGI(self, instrument: str) -> None:
3549        """
3550        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3551
3552        This method searches opened trade and orders of instrument throw all portfolio and then use
3553        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3554
3555        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3556
3557        :param instrument: string with FIGI id.
3558        """
3559        if instrument is None or not instrument:
3560            uLogger.error("FIGI id must be defined for using this method!")
3561            raise Exception("FIGI required")
3562
3563        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3564
3565        self._ticker = ""
3566        self._figi = instrument  # try to set instrument as FIGI id
3567
3568        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3569        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3570
3571        if limitAll and self.IsInLimitOrders(portfolio=overview):
3572            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3573            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3574
3575        if stopAll and self.IsInStopOrders(portfolio=overview):
3576            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3577            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3578
3579        if self.IsInPortfolio(portfolio=overview):
3580            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3581            self.CloseTrades(instruments=[instrument], portfolio=overview)
3582
3583    @staticmethod
3584    def ParseOrderParameters(operation, **inputParameters):
3585        """
3586        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3587
3588        :param operation: string "Buy" or "Sell".
3589        :param inputParameters: this is dict of strings that looks like this
3590               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3591               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3592               "prices" key: one or more prices to open limit-orders
3593               Counts of values in lots and prices lists must be equals!
3594        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3595        """
3596        # TODO: update order grid work with api v2
3597        pass
3598        # uLogger.debug("Input parameters: {}".format(inputParameters))
3599        #
3600        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3601        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3602        #     raise Exception("Incorrect value")
3603        #
3604        # if "l" in inputParameters.keys():
3605        #     inputParameters["lots"] = inputParameters.pop("l")
3606        #
3607        # if "p" in inputParameters.keys():
3608        #     inputParameters["prices"] = inputParameters.pop("p")
3609        #
3610        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3611        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3612        #     raise Exception("Incorrect value")
3613        #
3614        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3615        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3616        #
3617        # if len(lots) != len(prices):
3618        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3619        #     raise Exception("Incorrect value")
3620        #
3621        # uLogger.debug("Extracted parameters for orders:")
3622        # uLogger.debug("lots = {}".format(lots))
3623        # uLogger.debug("prices = {}".format(prices))
3624        #
3625        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3626        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3627        # uLogger.debug("Order parameters: {}".format(result))
3628        #
3629        # return result
3630
3631    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3632        """
3633        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3634
3635        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3636        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3637        """
3638        result = False
3639        msg = "Instrument not defined!"
3640
3641        if portfolio is None or not portfolio:
3642            portfolio = self.Overview(show=False)
3643
3644        if self._ticker:
3645            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3646            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3647
3648            for iType in TKS_INSTRUMENTS:
3649                for instrument in portfolio["stat"][iType]:
3650                    if instrument["ticker"] == self._ticker:
3651                        result = True
3652                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3653                        break
3654
3655        elif self._figi:
3656            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3657            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3658
3659            for iType in TKS_INSTRUMENTS:
3660                for instrument in portfolio["stat"][iType]:
3661                    if instrument["figi"] == self._figi:
3662                        result = True
3663                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3664                        break
3665
3666        else:
3667            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3668
3669        uLogger.debug(msg)
3670
3671        return result
3672
3673    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3674        """
3675        Returns instrument from the user's portfolio if it presents there.
3676        Instrument must be defined by `ticker` (highly priority) or `figi`.
3677
3678        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3679        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3680        """
3681        result = None
3682        msg = "Instrument not defined!"
3683
3684        if portfolio is None or not portfolio:
3685            portfolio = self.Overview(show=False)
3686
3687        if self._ticker:
3688            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3689            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3690
3691            for iType in TKS_INSTRUMENTS:
3692                for instrument in portfolio["stat"][iType]:
3693                    if instrument["ticker"] == self._ticker:
3694                        result = instrument
3695                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3696                        break
3697
3698        elif self._figi:
3699            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3700            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3701
3702            for iType in TKS_INSTRUMENTS:
3703                for instrument in portfolio["stat"][iType]:
3704                    if instrument["figi"] == self._figi:
3705                        result = instrument
3706                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3707                        break
3708
3709        else:
3710            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3711
3712        uLogger.debug(msg)
3713
3714        return result
3715
3716    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3717        """
3718        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3719
3720        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3721
3722        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3723        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3724        """
3725        result = False
3726        msg = "Instrument not defined!"
3727
3728        if portfolio is None or not portfolio:
3729            portfolio = self.Overview(show=False)
3730
3731        if self._ticker:
3732            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3733            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3734
3735            for instrument in portfolio["stat"]["orders"]:
3736                if instrument["ticker"] == self._ticker:
3737                    result = True
3738                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3739                    break
3740
3741        elif self._figi:
3742            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3743            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3744
3745            for instrument in portfolio["stat"]["orders"]:
3746                if instrument["figi"] == self._figi:
3747                    result = True
3748                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3749                    break
3750
3751        else:
3752            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3753
3754        uLogger.debug(msg)
3755
3756        return result
3757
3758    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3759        """
3760        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3761        Instrument must be defined by `ticker` (highly priority) or `figi`.
3762
3763        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3764
3765        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3766        :return: list with `orderID`s of limit orders.
3767        """
3768        result = []
3769        msg = "Instrument not defined!"
3770
3771        if portfolio is None or not portfolio:
3772            portfolio = self.Overview(show=False)
3773
3774        if self._ticker:
3775            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3776            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3777
3778            for instrument in portfolio["stat"]["orders"]:
3779                if instrument["ticker"] == self._ticker:
3780                    result.append(instrument["orderID"])
3781
3782            if result:
3783                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3784
3785        elif self._figi:
3786            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3787            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3788
3789            for instrument in portfolio["stat"]["orders"]:
3790                if instrument["figi"] == self._figi:
3791                    result.append(instrument["orderID"])
3792
3793            if result:
3794                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3795
3796        else:
3797            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3798
3799        uLogger.debug(msg)
3800
3801        return result
3802
3803    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3804        """
3805        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3806
3807        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3808
3809        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3810        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3811        """
3812        result = False
3813        msg = "Instrument not defined!"
3814
3815        if portfolio is None or not portfolio:
3816            portfolio = self.Overview(show=False)
3817
3818        if self._ticker:
3819            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3820            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3821
3822            for instrument in portfolio["stat"]["stopOrders"]:
3823                if instrument["ticker"] == self._ticker:
3824                    result = True
3825                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3826                    break
3827
3828        elif self._figi:
3829            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3830            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3831
3832            for instrument in portfolio["stat"]["stopOrders"]:
3833                if instrument["figi"] == self._figi:
3834                    result = True
3835                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3836                    break
3837
3838        else:
3839            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3840
3841        uLogger.debug(msg)
3842
3843        return result
3844
3845    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3846        """
3847        Returns list with all `orderID`s of opened stop orders for the instrument.
3848        Instrument must be defined by `ticker` (highly priority) or `figi`.
3849
3850        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3851
3852        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3853        :return: list with `orderID`s of stop orders.
3854        """
3855        result = []
3856        msg = "Instrument not defined!"
3857
3858        if portfolio is None or not portfolio:
3859            portfolio = self.Overview(show=False)
3860
3861        if self._ticker:
3862            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3863            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3864
3865            for instrument in portfolio["stat"]["stopOrders"]:
3866                if instrument["ticker"] == self._ticker:
3867                    result.append(instrument["orderID"])
3868
3869            if result:
3870                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3871
3872        elif self._figi:
3873            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3874            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3875
3876            for instrument in portfolio["stat"]["stopOrders"]:
3877                if instrument["figi"] == self._figi:
3878                    result.append(instrument["orderID"])
3879
3880            if result:
3881                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3882
3883        else:
3884            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3885
3886        uLogger.debug(msg)
3887
3888        return result
3889
3890    def RequestLimits(self) -> dict:
3891        """
3892        Method for obtaining the available funds for withdrawal for current `accountId`.
3893
3894        See also:
3895        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3896        - `OverviewLimits()` method
3897
3898        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3899                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3900                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3901                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3902        """
3903        if self.accountId is None or not self.accountId:
3904            uLogger.error("Variable `accountId` must be defined for using this method!")
3905            raise Exception("Account ID required")
3906
3907        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3908
3909        self.body = str({"accountId": self.accountId})
3910        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3911        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3912
3913        if self.moreDebug:
3914            uLogger.debug("Records about available funds for withdrawal successfully received")
3915
3916        return rawLimits
3917
3918    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3919        """
3920        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3921
3922        See also: `RequestLimits()`.
3923
3924        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3925        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3926        :return: dict with raw parsed data from server and some calculated statistics about it.
3927        """
3928        if self.accountId is None or not self.accountId:
3929            uLogger.error("Variable `accountId` must be defined for using this method!")
3930            raise Exception("Account ID required")
3931
3932        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3933
3934        view = {
3935            "rawLimits": rawLimits,
3936            "limits": {  # parsed data for every currency:
3937                "money": {  # this is an array of portfolio currency positions
3938                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3939                },
3940                "blocked": {  # this is an array of blocked currency
3941                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3942                },
3943                "blockedGuarantee": {  # this is locked money under collateral for futures
3944                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3945                },
3946            },
3947        }
3948
3949        # --- Prepare text table with limits in human-readable format:
3950        if show or onlyFiles:
3951            info = [
3952                "# Withdrawal limits\n\n",
3953                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3954                "* **Account ID:** [{}]\n".format(self.accountId),
3955            ]
3956
3957            if view["limits"]["money"]:
3958                info.extend([
3959                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3960                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3961                ])
3962
3963            else:
3964                info.append("\nNo withdrawal limits\n")
3965
3966            for curr in view["limits"]["money"].keys():
3967                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3968                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3969                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3970
3971                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3972                    "[{}]".format(curr),
3973                    "{:.2f}".format(view["limits"]["money"][curr]),
3974                    "{:.2f}".format(availableMoney),
3975                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3976                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3977                )
3978
3979                if curr == "rub":
3980                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3981
3982                else:
3983                    info.append(infoStr)
3984
3985            infoText = "".join(info)
3986
3987            if show and not onlyFiles:
3988                uLogger.info(infoText)
3989
3990            if self.withdrawalLimitsFile and (show or onlyFiles):
3991                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3992                    fH.write(infoText)
3993
3994                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3995
3996                if self.useHTMLReports:
3997                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3998                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3999                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4000
4001                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4002
4003        return view
4004
4005    def RequestAccounts(self) -> dict:
4006        """
4007        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4008
4009        See also:
4010        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4011        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4012        - `OverviewUserInfo()` method
4013
4014        :return: dict with raw data from server that contains accounts info. Example of dict:
4015                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4016                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4017                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4018                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4019        """
4020        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4021
4022        self.body = str({})
4023        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4024        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4025
4026        if self.moreDebug:
4027            uLogger.debug("Records about available accounts successfully received")
4028
4029        return rawAccounts
4030
4031    def RequestUserInfo(self) -> dict:
4032        """
4033        Method for requesting common user's information.
4034
4035        See also:
4036        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4037        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4038        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4039        - `OverviewUserInfo()` method
4040
4041        :return: dict with raw data from server that contains user's information. Example of dict:
4042                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4043                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4044        """
4045        uLogger.debug("Requesting common user's information. Wait, please...")
4046
4047        self.body = str({})
4048        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4049        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4050
4051        if self.moreDebug:
4052            uLogger.debug("Records about current user successfully received")
4053
4054        return rawUserInfo
4055
4056    def RequestMarginStatus(self, accountId: str = None) -> dict:
4057        """
4058        Method for requesting margin calculation for defined account ID.
4059
4060        See also:
4061        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4062        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4063        - `OverviewUserInfo()` method
4064
4065        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4066        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4067                 Example of responses:
4068                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4069                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4070                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4071                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4072                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4073                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4074        """
4075        if accountId is None or not accountId:
4076            if self.accountId is None or not self.accountId:
4077                uLogger.error("Variable `accountId` must be defined for using this method!")
4078                raise Exception("Account ID required")
4079
4080            else:
4081                accountId = self.accountId  # use `self.accountId` (main ID) by default
4082
4083        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4084
4085        self.body = str({"accountId": accountId})
4086        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4087        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4088
4089        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4090            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4091            rawMargin = {}
4092
4093        else:
4094            if self.moreDebug:
4095                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4096
4097        return rawMargin
4098
4099    def RequestTariffLimits(self) -> dict:
4100        """
4101        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4102
4103        See also:
4104        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4105        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4106        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4107        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4108        - `OverviewUserInfo()` method
4109
4110        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4111                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4112                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4113        """
4114        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4115
4116        self.body = str({})
4117        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4118        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4119
4120        if self.moreDebug:
4121            uLogger.debug("Records with limits of current tariff successfully received")
4122
4123        return rawTariffLimits
4124
4125    def RequestBondCoupons(self, iJSON: dict) -> dict:
4126        """
4127        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4128        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4129        All dates are in UTC timezone.
4130
4131        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4132        Documentation:
4133        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4134        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4135
4136        See also: `ExtendBondsData()`.
4137
4138        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4139                      If raw iJSON is not data of bond then server returns an error [400] with message:
4140                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4141        :return: dictionary with bond payment calendar. Response example
4142                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4143                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4144                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4145                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4146        """
4147        if iJSON["figi"] is None or not iJSON["figi"]:
4148            uLogger.error("FIGI must be defined for using this method!")
4149            raise Exception("FIGI required")
4150
4151        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4152        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4153
4154        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4155            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4156            self._figi,
4157            startDate,
4158            endDate,
4159        ))
4160
4161        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4162        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4163        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4164
4165        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4166            uLogger.warning("Instrument type is not bond!")
4167
4168        else:
4169            if self.moreDebug:
4170                uLogger.debug("Records about bond payment calendar successfully received")
4171
4172        return calendar
4173
4174    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4175        """
4176        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4177        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4178        coupon yields, current yields and some statistics etc.
4179
4180        WARNING! This is too long operation if a lot of bonds requested from broker server.
4181
4182        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4183
4184        :param instruments: list of strings with tickers or FIGIs.
4185        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4186                     for further used by data scientists or stock analytics.
4187        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4188                 In XLSX-file and Pandas DataFrame fields mean:
4189                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4190                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4191        """
4192        if instruments is None or not instruments:
4193            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4194            raise Exception("Ticker or FIGI required")
4195
4196        if isinstance(instruments, str):
4197            instruments = [instruments]
4198
4199        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4200
4201        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4202
4203        iCount = len(uniqueInstruments)
4204        tooLong = iCount >= 20
4205        if tooLong:
4206            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4207
4208        bonds = None
4209        for i, self._figi in enumerate(uniqueInstruments):
4210            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4211
4212            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4213                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4214                rawBond = self.SearchByFIGI(requestPrice=True)
4215
4216                # Widen raw data with UTC current time (iData["actualDateTime"]):
4217                actualDate = datetime.now(tzutc())
4218                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4219
4220                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4221                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4222
4223                # Replace some values with human-readable:
4224                iData["nominalCurrency"] = iData["nominal"]["currency"]
4225                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4226                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4227                iData["aciCurrency"] = iData["aciValue"]["currency"]
4228                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4229                iData["issueSize"] = int(iData["issueSize"])
4230                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4231                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4232                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4233                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4234                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4235                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4236                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4237                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4238                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4239                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4240
4241                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4242                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4243                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4244                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4245                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4246                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4247                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4248                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4249                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4250                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4251                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4252
4253                # Widen raw data with calendar data from `rawCalendar` values:
4254                calendarData = []
4255                if "events" in iData["rawCalendar"].keys():
4256                    for item in iData["rawCalendar"]["events"]:
4257                        calendarData.append({
4258                            "couponDate": item["couponDate"],
4259                            "couponNumber": int(item["couponNumber"]),
4260                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4261                            "payCurrency": item["payOneBond"]["currency"],
4262                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4263                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4264                            "couponStartDate": item["couponStartDate"],
4265                            "couponEndDate": item["couponEndDate"],
4266                            "couponPeriod": item["couponPeriod"],
4267                        })
4268
4269                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4270                    if "maturityDate" not in iData.keys():
4271                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4272
4273                # Widen raw data with Coupon Rate.
4274                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4275                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4276                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4277                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4278
4279                # Widen raw data with Yield to Maturity (YTM) on current date.
4280                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4281                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4282                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4283                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4284                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4285                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4286
4287                iData["calendar"] = calendarData  # adds calendar at the end
4288
4289                # Remove not used data:
4290                iData.pop("uid")
4291                iData.pop("positionUid")
4292                iData.pop("currentPrice")
4293                iData.pop("rawCalendar")
4294
4295                colNames = list(iData.keys())
4296                if bonds is None:
4297                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4298
4299                else:
4300                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4301
4302            else:
4303                uLogger.warning("Instrument is not a bond!")
4304
4305            processed = round(100 * (i + 1) / iCount, 1)
4306            if tooLong and processed % 5 == 0:
4307                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4308
4309            else:
4310                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4311
4312        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4313
4314        # Saving bonds from Pandas DataFrame to XLSX sheet:
4315        if xlsx and self.bondsXLSXFile:
4316            with pd.ExcelWriter(
4317                    path=self.bondsXLSXFile,
4318                    date_format=TKS_DATE_FORMAT,
4319                    datetime_format=TKS_DATE_TIME_FORMAT,
4320                    mode="w",
4321            ) as writer:
4322                bonds.to_excel(
4323                    writer,
4324                    sheet_name="Extended bonds data",
4325                    index=True,
4326                    encoding="UTF-8",
4327                    freeze_panes=(1, 1),
4328                )  # saving as XLSX-file with freeze first row and column as headers
4329
4330            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4331
4332        return bonds
4333
4334    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4335        """
4336        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4337
4338        WARNING! This is too long operation if a lot of bonds requested from broker server.
4339
4340        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4341
4342        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4343                        extended information about bonds: main info, current prices, bond payment calendar,
4344                        coupon yields, current yields and some statistics etc.
4345                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4346        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4347                     for further used by data scientists or stock analytics.
4348        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4349        """
4350        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4351            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4352
4353        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4354
4355        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4356        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4357        calendar = None
4358        for bond in extBonds.iterrows():
4359            for item in bond[1]["calendar"]:
4360                cData = {
4361                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4362                    "couponDate": item["couponDate"],
4363                    "figi": bond[1]["figi"],
4364                    "ticker": bond[1]["ticker"],
4365                    "name": bond[1]["name"],
4366                    "couponNumber": item["couponNumber"],
4367                    "payOneBond": item["payOneBond"],
4368                    "payCurrency": item["payCurrency"],
4369                    "couponType": item["couponType"],
4370                    "couponPeriod": item["couponPeriod"],
4371                    "fixDate": item["fixDate"],
4372                    "couponStartDate": item["couponStartDate"],
4373                    "couponEndDate": item["couponEndDate"],
4374                }
4375
4376                if calendar is None:
4377                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4378
4379                else:
4380                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4381
4382        if calendar is not None:
4383            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4384
4385            # Saving calendar from Pandas DataFrame to XLSX sheet:
4386            if xlsx:
4387                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4388
4389                with pd.ExcelWriter(
4390                        path=xlsxCalendarFile,
4391                        date_format=TKS_DATE_FORMAT,
4392                        datetime_format=TKS_DATE_TIME_FORMAT,
4393                        mode="w",
4394                ) as writer:
4395                    humanReadable = calendar.copy(deep=True)
4396                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4397                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4398                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4399                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4400                    humanReadable.columns = colNames  # human-readable column names
4401
4402                    humanReadable.to_excel(
4403                        writer,
4404                        sheet_name="Bond payments calendar",
4405                        index=False,
4406                        encoding="UTF-8",
4407                        freeze_panes=(1, 2),
4408                    )  # saving as XLSX-file with freeze first row and column as headers
4409
4410                    del humanReadable  # release df in memory
4411
4412                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4413
4414        return calendar
4415
4416    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4417        """
4418        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4419        Also, creates Markdown file with calendar data, `calendar.md` by default.
4420
4421        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4422
4423        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4424                        extended information about bonds: main info, current prices, bond payment calendar,
4425                        coupon yields, current yields and some statistics etc.
4426                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4427        :param show: if `True` then also printing bonds payment calendar to the console,
4428                     otherwise save to file `calendarFile` only. `False` by default.
4429        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4430        :return: multilines text in Markdown format with bonds payment calendar as a table.
4431        """
4432        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4433            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4434
4435        infoText = "# Bond payments calendar\n\n"
4436
4437        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4438
4439        if not (calendar is None or calendar.empty):
4440            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4441
4442            info = [
4443                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4444                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4445                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4446            ]
4447
4448            newMonth = False
4449            notOneBond = calendar["figi"].nunique() > 1
4450            for i, bond in enumerate(calendar.iterrows()):
4451                if newMonth and notOneBond:
4452                    info.append(splitLine)
4453
4454                info.append(
4455                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4456                        "  √" if bond[1]["paid"] else "  —",
4457                        bond[1]["couponDate"].split("T")[0],
4458                        bond[1]["figi"],
4459                        bond[1]["ticker"],
4460                        bond[1]["couponNumber"],
4461                        "{} {}".format(
4462                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4463                            bond[1]["payCurrency"],
4464                        ),
4465                        bond[1]["couponType"],
4466                        bond[1]["couponPeriod"],
4467                        bond[1]["fixDate"].split("T")[0],
4468                    )
4469                )
4470
4471                if i < len(calendar.values) - 1:
4472                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4473                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4474                    newMonth = False if curDate.month == nextDate.month else True
4475
4476                else:
4477                    newMonth = False
4478
4479            infoText += "".join(info)
4480
4481            if show and not onlyFiles:
4482                uLogger.info("{}".format(infoText))
4483
4484            if self.calendarFile is not None and (show or onlyFiles):
4485                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4486                    fH.write(infoText)
4487
4488                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4489
4490                if self.useHTMLReports:
4491                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4492                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4493                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4494
4495                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4496
4497        else:
4498            infoText += "No data\n"
4499
4500        return infoText
4501
4502    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4503        """
4504        Method for parsing and show simple table with all available user accounts.
4505
4506        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4507
4508        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4509        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4510        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4511                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4512                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4513                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4514                                                        "closed": "—", "access": "Full access" }, ...}}`
4515        """
4516        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4517
4518        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4519        accounts = {
4520            item["id"]: {
4521                "type": TKS_ACCOUNT_TYPES[item["type"]],
4522                "name": item["name"],
4523                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4524                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4525                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4526                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4527            } for item in rawAccounts["accounts"]
4528        }
4529
4530        # Raw and parsed data with some fields replaced in "stat" section:
4531        view = {
4532            "rawAccounts": rawAccounts,
4533            "stat": accounts,
4534        }
4535
4536        # --- Prepare simple text table with only accounts data in human-readable format:
4537        if show or onlyFiles:
4538            info = [
4539                "# User accounts\n\n",
4540                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4541                "| Account ID   | Type                      | Status                    | Name                           |\n",
4542                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4543            ]
4544
4545            for account in view["stat"].keys():
4546                info.extend([
4547                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4548                        account,
4549                        view["stat"][account]["type"],
4550                        view["stat"][account]["status"],
4551                        view["stat"][account]["name"],
4552                    )
4553                ])
4554
4555            infoText = "".join(info)
4556
4557            if show and not onlyFiles:
4558                uLogger.info(infoText)
4559
4560            if self.userAccountsFile and (show or onlyFiles):
4561                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4562                    fH.write(infoText)
4563
4564                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4565
4566                if self.useHTMLReports:
4567                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4568                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4569                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4570
4571                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4572
4573        return view
4574
4575    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4576        """
4577        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4578
4579        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4580
4581        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4582        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4583        :return: dict with raw parsed data from server and some calculated statistics about it.
4584        """
4585        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4586        tmpTicker = self._ticker
4587        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4588        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4589        self._ticker = tmpTicker
4590
4591        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4592        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4593        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4594        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4595        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4596        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4597
4598        # This is dict with parsed common user data:
4599        userInfo = {
4600            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4601            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4602            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4603            "tariff": rawUserInfo["tariff"],
4604        }
4605
4606        # This is an array of dict with parsed margin statuses for every account IDs:
4607        margins = {}
4608        for accountId in accounts.keys():
4609            if rawMargins[accountId]:
4610                margins[accountId] = {
4611                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4612                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4613                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4614                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4615                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4616                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4617                    "missing": missing["volume"],
4618                }
4619
4620            else:
4621                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4622
4623        unary = {}  # unary-connection limits
4624        for item in rawTariffLimits["unaryLimits"]:
4625            if item["limitPerMinute"] in unary.keys():
4626                unary[item["limitPerMinute"]].extend(item["methods"])
4627
4628            else:
4629                unary[item["limitPerMinute"]] = item["methods"]
4630
4631        stream = {}  # stream-connection limits
4632        for item in rawTariffLimits["streamLimits"]:
4633            if item["limit"] in stream.keys():
4634                stream[item["limit"]].extend(item["streams"])
4635
4636            else:
4637                stream[item["limit"]] = item["streams"]
4638
4639        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4640        limits = {
4641            "unary": unary,
4642            "stream": stream,
4643        }
4644
4645        # Raw and parsed data as an output result:
4646        view = {
4647            "rawUserInfo": rawUserInfo,
4648            "rawAccounts": rawAccounts,
4649            "rawMargins": rawMargins,
4650            "rawTariffLimits": rawTariffLimits,
4651            "stat": {
4652                "overview": overview,
4653                "userInfo": userInfo,
4654                "accounts": accounts,
4655                "margins": margins,
4656                "limits": limits,
4657            },
4658        }
4659
4660        # --- Prepare text table with user information in human-readable format:
4661        if show or onlyFiles:
4662            info = [
4663                "# Full user information\n\n",
4664                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4665                "## Common information\n\n",
4666                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4667                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4668                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4669                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4670                "\n## User accounts\n\n",
4671            ]
4672
4673            for account in view["stat"]["accounts"].keys():
4674                info.extend([
4675                    "### ID: [{}]\n\n".format(account),
4676                    "| Parameters           | Values                                                       |\n",
4677                    "|----------------------|--------------------------------------------------------------|\n",
4678                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4679                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4680                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4681                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4682                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4683                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4684                ])
4685
4686                if margins[account]:
4687                    info.extend([
4688                        "| Margin status:       | Enabled                                                      |\n",
4689                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4690                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4691                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4692                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4693                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4694                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4695                    ])
4696
4697                else:
4698                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4699
4700            info.extend([
4701                "\n## Current user tariff limits\n",
4702                "\n### See also\n",
4703                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4704                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4705                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4706                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4707                "\n### Unary limits\n",
4708            ])
4709
4710            if unary:
4711                for key, values in sorted(unary.items()):
4712                    info.append("\n* Max requests per minute: {}\n".format(key))
4713
4714                    for value in values:
4715                        info.append("  - {}\n".format(value))
4716
4717            else:
4718                info.append("\nNot available\n")
4719
4720            info.append("\n### Stream limits\n")
4721
4722            if stream:
4723                for key, values in sorted(stream.items()):
4724                    info.append("\n* Max stream connections: {}\n".format(key))
4725
4726                    for value in values:
4727                        info.append("  - {}\n".format(value))
4728
4729            else:
4730                info.append("\nNot available\n")
4731
4732            infoText = "".join(info)
4733
4734            if show and not onlyFiles:
4735                uLogger.info(infoText)
4736
4737            if self.userInfoFile and (show or onlyFiles):
4738                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4739                    fH.write(infoText)
4740
4741                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4742
4743                if self.useHTMLReports:
4744                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4745                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4746                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4747
4748                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4749
4750        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 87        """
 88        Main class init.
 89
 90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 93        :param useCache: use default cache file with raw data to use instead of `iList`.
 94                         True by default. Cache is auto-update if new day has come.
 95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 96        :param defaultCache: path to default cache file. `dump.json` by default.
 97        """
 98        if token is None or not token:
 99            try:
100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
102
103            except KeyError:
104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
105                raise Exception("Token required")
106
107        else:
108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
110
111        if accountId is None or not accountId:
112            try:
113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
115
116            except KeyError:
117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
118
119        else:
120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
122
123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
125
126        Latest version: https://pypi.org/project/tksbrokerapi/
127        """
128
129        self.__lock = Lock()  # initialize multiprocessing mutex lock
130
131        self.aliases = TKS_TICKER_ALIASES
132        """Some aliases instead official tickers.
133
134        See also: `TKSEnums.TKS_TICKER_ALIASES`
135        """
136
137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
138
139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
140
141        self._ticker = ""
142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
143
144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
146
147        See also: `SearchByTicker()`, `SearchInstruments()`.
148        """
149
150        self._figi = ""
151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
152
153        See also: `SearchByFIGI()`, `SearchInstruments()`.
154        """
155
156        self.depth = 1
157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
158
159        See also: `GetCurrentPrices()`.
160        """
161
162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
164
165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
166        """
167
168        uLogger.debug("Broker API server: {}".format(self.server))
169
170        self.timeout = 15
171        """Server operations timeout in seconds. Default: `15`.
172
173        See also: `SendAPIRequest()`.
174        """
175
176        self.headers = {
177            "Content-Type": "application/json",
178            "accept": "application/json",
179            "Authorization": "Bearer {}".format(self.token),
180            "x-app-name": "Tim55667757.TKSBrokerAPI",
181        }
182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
183
184        See also: `SendAPIRequest()`.
185        """
186
187        self.body = None
188        """Request body which send to broker server. Default: `None`.
189
190        See also: `SendAPIRequest()`.
191        """
192
193        self.moreDebug = False
194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
195
196        self.useHTMLReports = False
197        """
198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
199        
200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
201        """
202
203        self.historyFile = None
204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
205
206        See also: `History()`.
207        """
208
209        self.htmlHistoryFile = "index.html"
210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
211
212        See also: `ShowHistoryChart()`.
213        """
214
215        self.instrumentsFile = "instruments.md"
216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
217
218        See also: `ShowInstrumentsInfo()`.
219        """
220
221        self.searchResultsFile = "search-results.md"
222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
223
224        See also: `SearchInstruments()`.
225        """
226
227        self.pricesFile = "prices.md"
228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
229
230        See also: `GetListOfPrices()`.
231        """
232
233        self.infoFile = "info.md"
234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
235
236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
237        """
238
239        self.bondsXLSXFile = "ext-bonds.xlsx"
240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
242
243        See also: `ExtendBondsData()`.
244        """
245
246        self.calendarFile = "calendar.md"
247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
248        
249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
250
251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
252        """
253
254        self.overviewFile = "overview.md"
255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
256
257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
258        """
259
260        self.overviewDigestFile = "overview-digest.md"
261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
262
263        See also: `Overview()` with parameter `details="digest"`.
264        """
265
266        self.overviewPositionsFile = "overview-positions.md"
267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
268
269        See also: `Overview()` with parameter `details="positions"`.
270        """
271
272        self.overviewOrdersFile = "overview-orders.md"
273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
274
275        See also: `Overview()` with parameter `details="orders"`.
276        """
277
278        self.overviewAnalyticsFile = "overview-analytics.md"
279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
280
281        See also: `Overview()` with parameter `details="analytics"`.
282        """
283
284        self.overviewBondsCalendarFile = "overview-calendar.md"
285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
286
287        See also: `Overview()` with parameter `details="calendar"`.
288        """
289
290        self.reportFile = "deals.md"
291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
292
293        See also: `Deals()`.
294        """
295
296        self.withdrawalLimitsFile = "limits.md"
297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
298
299        See also: `OverviewLimits()` and `RequestLimits()`.
300        """
301
302        self.userInfoFile = "user-info.md"
303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
304
305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
306        """
307
308        self.userAccountsFile = "accounts.md"
309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
310
311        See also: `OverviewAccounts()`, `RequestAccounts()`.
312        """
313
314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
316
317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
318
319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
320        """
321
322        self.iList = None  # init iList for raw instruments data
323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
324        
325        See also: `Listing()`, `DumpInstruments()`.
326        """
327
328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
329        if useCache:
330            if os.path.exists(self.iListDumpFile):
331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
332                curTime = datetime.now(tzutc())
333
334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
336
337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
338
339                else:
340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
341
342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
343                        os.path.abspath(self.iListDumpFile),
344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
345                    ))
346
347            else:
348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
350
351        else:
352            self.iList = self.Listing()  # request new raw instruments data from broker server
353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
354
355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
357
358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
359        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

useHTMLReports

If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.

See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

ticker: str

Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
419    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
420        """
421        Send GET or POST request to broker server and receive JSON object.
422
423        self.header: must be defining with dictionary of headers.
424        self.body: if define then used as request body. None by default.
425        self.timeout: global request timeout, 15 seconds by default.
426        :param url: url with REST request.
427        :param reqType: send "GET" or "POST" request. "GET" by default.
428        :param retry: how many times retry after first request if an 5xx server errors occurred.
429        :param pause: sleep time in seconds between retries.
430        :return: response JSON (dictionary) from broker.
431        """
432        if reqType.upper() not in ("GET", "POST"):
433            uLogger.error("You can define request type: `GET` or `POST`!")
434            raise Exception("Incorrect value")
435
436        if self.moreDebug:
437            uLogger.debug("Request parameters:")
438            uLogger.debug("    - REST API URL: {}".format(url))
439            uLogger.debug("    - request type: {}".format(reqType))
440            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
441            uLogger.debug("    - body:\n{}".format(self.body))
442
443        # fast hack to avoid all operations with some tickers/FIGI
444        responseJSON = {}
445        oK = True
446        for item in self.exclude:
447            if item in url:
448                if self.moreDebug:
449                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
450
451                oK = False
452                break
453
454        if oK:
455            with self.__lock:  # acquire the mutex lock
456                counter = 0
457                response = None
458                errMsg = ""
459
460                while not response and counter <= retry:
461                    if reqType == "GET":
462                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
463
464                    if reqType == "POST":
465                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
466
467                    if self.moreDebug:
468                        uLogger.debug("Response:")
469                        uLogger.debug("    - status code: {}".format(response.status_code))
470                        uLogger.debug("    - reason: {}".format(response.reason))
471                        uLogger.debug("    - body length: {}".format(len(response.text)))
472                        uLogger.debug("    - headers:\n{}".format(response.headers))
473
474                    # Server returns some headers:
475                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
476                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
477                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
478                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
479                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
480                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
481                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
482                        sleep(rateLimitWait)
483
484                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
485                    if 400 <= response.status_code < 500:
486                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
487                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
488
489                        if "code" in response.text and "message" in response.text:
490                            msgDict = self._ParseJSON(rawData=response.text)
491                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
492
493                        counter = retry + 1  # do not retry for 4xx errors
494
495                    if 500 <= response.status_code < 600:
496                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
497                        uLogger.debug("    - not oK, {}".format(errMsg))
498
499                        if "code" in response.text and "message" in response.text:
500                            errMsgDict = self._ParseJSON(rawData=response.text)
501                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
502
503                        counter += 1
504
505                        if counter <= retry:
506                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
507                            sleep(pause)
508
509                responseJSON = self._ParseJSON(rawData=response.text)
510
511                if errMsg:
512                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
513                    uLogger.error("    - not oK, {}".format(errMsg))
514
515        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
548    def Listing(self) -> dict:
549        """
550        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
551
552        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
553        """
554        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
555        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
556
557        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
558        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
559        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
560
561        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
562        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
563        poolUpdater.close()  # close the thread pool
564        poolUpdater.join()  # wait a moment until all data returns from threads
565
566        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
567        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
568        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
569
570        # calculate minimum price increment (step) for all instruments and set up instrument's type:
571        for iType in iList.keys():
572            for ticker in iList[iType]:
573                iList[iType][ticker]["type"] = iType
574
575                if "minPriceIncrement" in iList[iType][ticker].keys():
576                    iList[iType][ticker]["step"] = NanoToFloat(
577                        iList[iType][ticker]["minPriceIncrement"]["units"],
578                        iList[iType][ticker]["minPriceIncrement"]["nano"],
579                    )
580
581                else:
582                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
583
584        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
586    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
587        """
588        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
589
590        See also: `DumpInstruments()`, `Listing()`.
591
592        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
593                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
594        """
595        if self.iListDumpFile is None or not self.iListDumpFile:
596            uLogger.error("Output name of dump file must be defined!")
597            raise Exception("Filename required")
598
599        if not self.iList or forceUpdate:
600            self.iList = self.Listing()
601
602        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
603
604        # Save as XLSX with separated sheets for every type of instruments:
605        with pd.ExcelWriter(
606                path=xlsxDumpFile,
607                date_format=TKS_DATE_FORMAT,
608                datetime_format=TKS_DATE_TIME_FORMAT,
609                mode="w",
610        ) as writer:
611            for iType in TKS_INSTRUMENTS:
612                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
613                df = df[sorted(df)]  # sorted by column names
614                df = df.applymap(
615                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
616                    na_action="ignore",
617                )  # converting numbers from nano-type to float in every cell
618                df.to_excel(
619                    writer,
620                    sheet_name=iType,
621                    encoding="UTF-8",
622                    freeze_panes=(1, 1),
623                )  # saving as XLSX-file with freeze first row and column as headers
624
625        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
627    def DumpInstruments(self, forceUpdate: bool = True) -> str:
628        """
629        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
630        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
631
632        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
633
634        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
635                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
636        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
637        """
638        if self.iListDumpFile is None or not self.iListDumpFile:
639            uLogger.error("Output name of dump file must be defined!")
640            raise Exception("Filename required")
641
642        if not self.iList or forceUpdate:
643            self.iList = self.Listing()
644
645        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
646        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
647            fH.write(jsonDump)
648
649        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
650
651        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
653    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True, onlyFiles=False) -> str:
654        """
655        Show information about one instrument defined by json data and prints it in Markdown format.
656
657        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
658
659        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
660        :param show: if `True` then also printing information about instrument and its current price.
661        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
662        :return: multilines text in Markdown format with information about one instrument.
663        """
664        splitLine = "|                                                             |                                                        |\n"
665        infoText = ""
666
667        if iJSON is not None and iJSON and isinstance(iJSON, dict):
668            info = [
669                "# Main information\n\n",
670                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
671                "| Parameters                                                  | Values                                                 |\n",
672                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
673                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
674                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
675            ]
676
677            if "sector" in iJSON.keys() and iJSON["sector"]:
678                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
679
680            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
681                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
682
683            info.extend([
684                splitLine,
685                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
686                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
687            ])
688
689            if "isin" in iJSON.keys() and iJSON["isin"]:
690                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
691
692            if "classCode" in iJSON.keys():
693                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
694
695            info.extend([
696                splitLine,
697                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
698                splitLine,
699                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
700                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
701                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
702            ])
703
704            if iJSON["figi"]:
705                self._figi = iJSON["figi"]
706                iJSON = iJSON | self.RequestTradingStatus()
707
708                info.extend([
709                    splitLine,
710                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
711                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
712                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
713                ])
714
715            info.append(splitLine)
716
717            if "type" in iJSON.keys() and iJSON["type"]:
718                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
719
720                if "shareType" in iJSON.keys() and iJSON["shareType"]:
721                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
722
723            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
724                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
725
726            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
727                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
728
729            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
730                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
731
732            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
733                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
734
735            if "focusType" in iJSON.keys() and iJSON["focusType"]:
736                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
737
738            if "assetType" in iJSON.keys() and iJSON["assetType"]:
739                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
740
741            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
742                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
743
744            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
745                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
746
747            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
748                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
749
750            if "currency" in iJSON.keys():
751                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
752
753            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
754                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
755
756            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
757                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
758
759            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
760                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
761
762            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
763                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
764
765            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
766                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
767
768            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
769                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
770
771            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
772                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
773
774            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
775                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
776
777            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
778                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
779
780            iExt = None
781            if iJSON["type"] == "Bonds":
782                info.extend([
783                    splitLine,
784                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
785                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
786                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
787                        iJSON["nominal"]["currency"],
788                    )),
789                ])
790
791                if "floatingCouponFlag" in iJSON.keys():
792                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
793
794                if "amortizationFlag" in iJSON.keys():
795                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
796
797                info.append(splitLine)
798
799                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
800                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
801
802                if iJSON["figi"]:
803                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
804
805                    info.extend([
806                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
807                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
808                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
809                    ])
810
811                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
812                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
813                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
814                        iJSON["aciValue"]["currency"]
815                    )))
816
817            if "currentPrice" in iJSON.keys():
818                info.append(splitLine)
819
820                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
821                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
822
823                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
824                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
825                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
826                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
827                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
828
829                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
830                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
831
832                info.extend([
833                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
834                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
835                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
836                    )),
837                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
838                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
839                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
840                    )),
841                    "| Changes between last deal price and last close              | {:<54} |\n".format(
842                        "{:.2f}%{}".format(
843                            iJSON["currentPrice"]["changes"],
844                            " ({}{:.2f} {})".format(
845                                "+" if bondChangesDelta > 0 else "",
846                                bondChangesDelta,
847                                aciCurrency
848                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
849                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
850                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
851                                currency
852                            ),
853                        )
854                    ),
855                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
856                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
857                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
858                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
859                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
860                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
861                    )),
862                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
863                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
864                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
865                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
866                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
867                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
868                    )),
869                ])
870
871            if "lot" in iJSON.keys():
872                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
873
874            if "step" in iJSON.keys() and iJSON["step"] != 0:
875                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
876
877            # Add bond payment calendar:
878            if iJSON["type"] == "Bonds":
879                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
880                info.extend(["\n#", strCalendar])
881
882            infoText += "".join(info)
883
884            if show and not onlyFiles:
885                uLogger.info("{}".format(infoText))
886
887            if self.infoFile is not None and (show or onlyFiles):
888                with open(self.infoFile, "w", encoding="UTF-8") as fH:
889                    fH.write(infoText)
890
891                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
892
893                if self.useHTMLReports:
894                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
895                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
896                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
897
898                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
899
900        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
902    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
903        """
904        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
905
906        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
907        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
908        :return: JSON formatted data with information about instrument.
909        """
910        tickerJSON = {}
911        if self.moreDebug:
912            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
913
914        if not self._ticker:
915            uLogger.warning("self._ticker variable is not be empty!")
916
917        else:
918            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
919                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
920                raise Exception("Instrument not allowed")
921
922            if not self.iList:
923                self.iList = self.Listing()
924
925            if self._ticker in self.iList["Shares"].keys():
926                tickerJSON = self.iList["Shares"][self._ticker]
927                if self.moreDebug:
928                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
929
930            elif self._ticker in self.iList["Currencies"].keys():
931                tickerJSON = self.iList["Currencies"][self._ticker]
932                if self.moreDebug:
933                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
934
935            elif self._ticker in self.iList["Bonds"].keys():
936                tickerJSON = self.iList["Bonds"][self._ticker]
937                if self.moreDebug:
938                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
939
940            elif self._ticker in self.iList["Etfs"].keys():
941                tickerJSON = self.iList["Etfs"][self._ticker]
942                if self.moreDebug:
943                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
944
945            elif self._ticker in self.iList["Futures"].keys():
946                tickerJSON = self.iList["Futures"][self._ticker]
947                if self.moreDebug:
948                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
949
950        if tickerJSON:
951            self._figi = tickerJSON["figi"]
952
953            if requestPrice:
954                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
955
956                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
957                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
958
959                else:
960                    tickerJSON["currentPrice"]["changes"] = 0
961
962            if show:
963                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
964
965        else:
966            if show:
967                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
968
969        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 971    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 972        """
 973        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 974
 975        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 976        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 977        :return: JSON formatted data with information about instrument.
 978        """
 979        figiJSON = {}
 980        if self.moreDebug:
 981            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 982
 983        if not self._figi:
 984            uLogger.warning("self._figi variable is not be empty!")
 985
 986        else:
 987            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 988                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 989                raise Exception("Instrument not allowed")
 990
 991            if not self.iList:
 992                self.iList = self.Listing()
 993
 994            for item in self.iList["Shares"].keys():
 995                if self._figi == self.iList["Shares"][item]["figi"]:
 996                    figiJSON = self.iList["Shares"][item]
 997
 998                    if self.moreDebug:
 999                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1000
1001                    break
1002
1003            if not figiJSON:
1004                for item in self.iList["Currencies"].keys():
1005                    if self._figi == self.iList["Currencies"][item]["figi"]:
1006                        figiJSON = self.iList["Currencies"][item]
1007
1008                        if self.moreDebug:
1009                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1010
1011                        break
1012
1013            if not figiJSON:
1014                for item in self.iList["Bonds"].keys():
1015                    if self._figi == self.iList["Bonds"][item]["figi"]:
1016                        figiJSON = self.iList["Bonds"][item]
1017
1018                        if self.moreDebug:
1019                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1020
1021                        break
1022
1023            if not figiJSON:
1024                for item in self.iList["Etfs"].keys():
1025                    if self._figi == self.iList["Etfs"][item]["figi"]:
1026                        figiJSON = self.iList["Etfs"][item]
1027
1028                        if self.moreDebug:
1029                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1030
1031                        break
1032
1033            if not figiJSON:
1034                for item in self.iList["Futures"].keys():
1035                    if self._figi == self.iList["Futures"][item]["figi"]:
1036                        figiJSON = self.iList["Futures"][item]
1037
1038                        if self.moreDebug:
1039                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1040
1041                        break
1042
1043        if figiJSON:
1044            self._figi = figiJSON["figi"]
1045            self._ticker = figiJSON["ticker"]
1046
1047            if requestPrice:
1048                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1049
1050                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1051                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1052
1053                else:
1054                    figiJSON["currentPrice"]["changes"] = 0
1055
1056            if show:
1057                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1058
1059        else:
1060            if show:
1061                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1062
1063        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1065    def GetCurrentPrices(self, show: bool = True) -> dict:
1066        """
1067        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1068        `{"buy": [{"price": 1243.8, "quantity": 193},
1069                  {"price": 1244.0, "quantity": 168},
1070                  {"price": 1244.8, "quantity": 5},
1071                  {"price": 1245.0, "quantity": 61},
1072                  {"price": 1245.4, "quantity": 60}],
1073          "sell": [{"price": 1243.6, "quantity": 8},
1074                   {"price": 1242.6, "quantity": 10},
1075                   {"price": 1242.4, "quantity": 18},
1076                   {"price": 1242.2, "quantity": 50},
1077                   {"price": 1242.0, "quantity": 113}],
1078          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1079        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1080        - sell: list of dicts with Buyers prices,
1081            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1082            - quantity: volume value by current price in lots,
1083        - limitUp: current trade session limit price, maximum,
1084        - limitDown: current trade session limit price, minimum,
1085        - lastPrice: last deal price of the instrument,
1086        - closePrice: previous trade session close price of the instrument.
1087
1088        See also: `SearchByTicker()` and `SearchByFIGI()`.
1089        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1090        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1091
1092        :param show: if `True` then print DOM to log and console.
1093        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1094                 If an error occurred then returns an empty record:
1095                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1096        """
1097        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1098
1099        if self.depth < 1:
1100            uLogger.error("Depth of Market (DOM) must be >=1!")
1101            raise Exception("Incorrect value")
1102
1103        if not (self._ticker or self._figi):
1104            uLogger.error("self._ticker or self._figi variables must be defined!")
1105            raise Exception("Ticker or FIGI required")
1106
1107        if self._ticker and not self._figi:
1108            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1109            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1110
1111        if not self._ticker and self._figi:
1112            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1113            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1114
1115        if not self._figi:
1116            uLogger.error("FIGI is not defined!")
1117            raise Exception("Ticker or FIGI required")
1118
1119        else:
1120            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1121
1122            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1123            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1124            self.body = str({"figi": self._figi, "depth": self.depth})
1125            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1126
1127            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1128                # list of dicts with sellers orders:
1129                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1130
1131                # list of dicts with buyers orders:
1132                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1133
1134                # max price of instrument at this time:
1135                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1136
1137                # min price of instrument at this time:
1138                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1139
1140                # last price of deal with instrument:
1141                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1142
1143                # last close price of instrument:
1144                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1145
1146            else:
1147                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1148                uLogger.debug("Server response: {}".format(pricesResponse))
1149
1150            if show:
1151                if prices["buy"] or prices["sell"]:
1152                    info = [
1153                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1154                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1155                            self._ticker,
1156                            self._figi,
1157                            self.depth,
1158                        ),
1159                        "-" * 60, "\n",
1160                        "             Orders of Buyers | Orders of Sellers\n",
1161                        "-" * 60, "\n",
1162                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1163                        "-" * 60, "\n",
1164                    ]
1165
1166                    if not prices["buy"]:
1167                        info.append("                              | No orders!\n")
1168                        sumBuy = 0
1169
1170                    else:
1171                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1172                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1173                        for item in maxMinSorted:
1174                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1175
1176                    if not prices["sell"]:
1177                        info.append("No orders!                    |\n")
1178                        sumSell = 0
1179
1180                    else:
1181                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1182                        for item in prices["sell"]:
1183                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1184
1185                    info.extend([
1186                        "-" * 60, "\n",
1187                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1188                        "-" * 60, "\n",
1189                    ])
1190
1191                    infoText = "".join(info)
1192
1193                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1194
1195                else:
1196                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1197
1198        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1200    def ShowInstrumentsInfo(self, show: bool = True, onlyFiles=False) -> str:
1201        """
1202        This method get and show information about all available broker instruments for current user account.
1203        If `instrumentsFile` string is not empty then also save information to this file.
1204
1205        :param show: if `True` then print results to console, if `False` — print only to file.
1206        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1207        :return: multi-lines string with all available broker instruments.
1208        """
1209        if not self.iList:
1210            self.iList = self.Listing()
1211
1212        info = [
1213            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1214            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1215        ]
1216
1217        # add instruments count by type:
1218        for iType in self.iList.keys():
1219            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1220
1221        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1222        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1223
1224        # generating info tables with all instruments by type:
1225        for iType in self.iList.keys():
1226            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1227
1228            for instrument in self.iList[iType].keys():
1229                iName = self.iList[iType][instrument]["name"]  # instrument's name
1230                if len(iName) > 57:
1231                    iName = "{}...".format(iName[:54])  # right trim for a long string
1232
1233                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1234                    self.iList[iType][instrument]["ticker"],
1235                    iName,
1236                    self.iList[iType][instrument]["figi"],
1237                    self.iList[iType][instrument]["currency"],
1238                    self.iList[iType][instrument]["lot"],
1239                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1240                ))
1241
1242        infoText = "".join(info)
1243
1244        if show and not onlyFiles:
1245            uLogger.info(infoText)
1246
1247        if self.instrumentsFile and (show or onlyFiles):
1248            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1249                fH.write(infoText)
1250
1251            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1252
1253            if self.useHTMLReports:
1254                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1255                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1256                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1257
1258                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1259
1260        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multi-lines string with all available broker instruments.

def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1262    def SearchInstruments(self, pattern: str, show: bool = True, onlyFiles=False) -> dict:
1263        """
1264        This method search and show information about instruments by part of its ticker, FIGI or name.
1265        If `searchResultsFile` string is not empty then also save information to this file.
1266
1267        :param pattern: string with part of ticker, FIGI or instrument's name.
1268        :param show: if `True` then print results to console, if `False` — return list of result only.
1269        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1270        :return: list of dictionaries with all found instruments.
1271        """
1272        if not self.iList:
1273            self.iList = self.Listing()
1274
1275        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1276        compiledPattern = re.compile(pattern, re.IGNORECASE)
1277
1278        for iType in self.iList:
1279            for instrument in self.iList[iType].values():
1280                searchResult = compiledPattern.search(" ".join(
1281                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1282                ))
1283
1284                if searchResult:
1285                    searchResults[iType][instrument["ticker"]] = instrument
1286
1287        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1288        info = [
1289            "# Search results\n\n",
1290            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1291            "* **Search pattern:** [{}]\n".format(pattern),
1292            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1293            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1294        ]
1295        infoShort = info[:]
1296
1297        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1298        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1299        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1300
1301        if resultsLen == 0:
1302            info.append("\nNo results\n")
1303            infoShort.append("\nNo results\n")
1304            uLogger.warning("No results. Try changing your search pattern.")
1305
1306        else:
1307            for iType in searchResults:
1308                iTypeValuesCount = len(searchResults[iType].values())
1309                if iTypeValuesCount > 0:
1310                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1311                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1312
1313                    for instrument in searchResults[iType].values():
1314                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1315                            instrument["type"],
1316                            instrument["ticker"],
1317                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1318                            instrument["figi"],
1319                        ))
1320
1321                    if iTypeValuesCount <= 5:
1322                        infoShort.extend(info[-iTypeValuesCount:])
1323
1324                    else:
1325                        infoShort.extend(info[-5:])
1326                        infoShort.append(skippedLine)
1327
1328        infoText = "".join(info)
1329        infoTextShort = "".join(infoShort)
1330
1331        if show and not onlyFiles:
1332            uLogger.info(infoTextShort)
1333            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1334
1335        if self.searchResultsFile and (show or onlyFiles):
1336            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1337                fH.write(infoText)
1338
1339            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1340
1341            if self.useHTMLReports:
1342                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1343                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1344                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1345
1346                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1347
1348        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1350    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1351        """
1352        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1353
1354        :param instruments: list of strings with tickers or FIGIs.
1355        :return: list with unique instrument FIGIs only.
1356        """
1357        requestedInstruments = []
1358        for iName in instruments:
1359            if iName not in self.aliases.keys():
1360                if iName not in requestedInstruments:
1361                    requestedInstruments.append(iName)
1362
1363            else:
1364                if iName not in requestedInstruments:
1365                    if self.aliases[iName] not in requestedInstruments:
1366                        requestedInstruments.append(self.aliases[iName])
1367
1368        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1369
1370        onlyUniqueFIGIs = []
1371        for iName in requestedInstruments:
1372            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1373                continue
1374
1375            self._ticker = iName
1376            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1377
1378            if not iData:
1379                self._ticker = ""
1380                self._figi = iName
1381
1382                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1383
1384                if not iData:
1385                    self._figi = ""
1386                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1387
1388            if iData and iData["figi"] not in onlyUniqueFIGIs:
1389                onlyUniqueFIGIs.append(iData["figi"])
1390
1391        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1392
1393        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices( self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1395    def GetListOfPrices(self, instruments: list[str], show: bool = False, onlyFiles=False) -> list[dict]:
1396        """
1397        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1398
1399        See limits: https://tinkoff.github.io/investAPI/limits/
1400
1401        If `pricesFile` string is not empty then also save information to this file.
1402
1403        :param instruments: list of strings with tickers or FIGIs.
1404        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1405        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1406        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1407                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1408        """
1409        if instruments is None or not instruments:
1410            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1411            raise Exception("Ticker or FIGI required")
1412
1413        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1414
1415        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1416
1417        iList = []  # trying to get info and current prices about all unique instruments:
1418        for self._figi in onlyUniqueFIGIs:
1419            iData = self.SearchByFIGI(requestPrice=True, show=False)
1420            iList.append(iData)
1421
1422        self.ShowListOfPrices(iList, show, onlyFiles)
1423
1424        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1426    def ShowListOfPrices(self, iList: list, show: bool = True, onlyFiles=False) -> str:
1427        """
1428        Show table contains current prices of given instruments.
1429
1430        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1431                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1432        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1433        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1434        :return: multilines text in Markdown format as a table contains current prices.
1435        """
1436        infoText = ""
1437
1438        if show or self.pricesFile or onlyFiles:
1439            info = [
1440                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1441                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1442                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1443            ]
1444
1445            for item in iList:
1446                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1447                    item["ticker"],
1448                    item["figi"],
1449                    item["type"],
1450                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1451                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1452                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1453                    "{} / {}".format(
1454                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1455                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1456                    ),
1457                    "{} / {}".format(
1458                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1459                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1460                    ),
1461                    item["currency"],
1462                ))
1463
1464            infoText = "".join(info)
1465
1466            if show and not onlyFiles:
1467                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1468
1469            if self.pricesFile and (show or onlyFiles):
1470                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1471                    fH.write(infoText)
1472
1473                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1474
1475                if self.useHTMLReports:
1476                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1477                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1478                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1479
1480                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1481
1482        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1484    def RequestTradingStatus(self) -> dict:
1485        """
1486        Requesting trading status for the instrument defined by `figi` variable.
1487
1488        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1489
1490        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1491
1492        :return: dictionary with trading status attributes. Response example:
1493                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1494                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1495        """
1496        if self._figi is None or not self._figi:
1497            uLogger.error("Variable `figi` must be defined for using this method!")
1498            raise Exception("FIGI required")
1499
1500        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1501
1502        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1503        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1504        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1505
1506        if self.moreDebug:
1507            uLogger.debug("Records about current trading status successfully received")
1508
1509        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1511    def RequestPortfolio(self) -> dict:
1512        """
1513        Requesting actual user's portfolio for current `accountId`.
1514
1515        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1516
1517        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1518
1519        :return: dictionary with user's portfolio.
1520        """
1521        if self.accountId is None or not self.accountId:
1522            uLogger.error("Variable `accountId` must be defined for using this method!")
1523            raise Exception("Account ID required")
1524
1525        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1526
1527        self.body = str({"accountId": self.accountId})
1528        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1529        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1530
1531        if self.moreDebug:
1532            uLogger.debug("Records about user's portfolio successfully received")
1533
1534        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1536    def RequestPositions(self) -> dict:
1537        """
1538        Requesting open positions by currencies and instruments for current `accountId`.
1539
1540        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1541
1542        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1543
1544        :return: dictionary with open positions by instruments.
1545        """
1546        if self.accountId is None or not self.accountId:
1547            uLogger.error("Variable `accountId` must be defined for using this method!")
1548            raise Exception("Account ID required")
1549
1550        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1551
1552        self.body = str({"accountId": self.accountId})
1553        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1554        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1555
1556        if self.moreDebug:
1557            uLogger.debug("Records about current open positions successfully received")
1558
1559        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1561    def RequestPendingOrders(self) -> list:
1562        """
1563        Requesting current actual pending limit orders for current `accountId`.
1564
1565        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1566
1567        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1568
1569        :return: list of dictionaries with pending limit orders.
1570        """
1571        if self.accountId is None or not self.accountId:
1572            uLogger.error("Variable `accountId` must be defined for using this method!")
1573            raise Exception("Account ID required")
1574
1575        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1576
1577        self.body = str({"accountId": self.accountId})
1578        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1579        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1580
1581        if "orders" in rawResponse.keys():
1582            rawOrders = rawResponse["orders"]
1583            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1584
1585        else:
1586            rawOrders = []
1587            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1588
1589        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1591    def RequestStopOrders(self) -> list:
1592        """
1593        Requesting current actual stop orders for current `accountId`.
1594
1595        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1596
1597        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1598
1599        :return: list of dictionaries with stop orders.
1600        """
1601        if self.accountId is None or not self.accountId:
1602            uLogger.error("Variable `accountId` must be defined for using this method!")
1603            raise Exception("Account ID required")
1604
1605        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1606
1607        self.body = str({"accountId": self.accountId})
1608        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1609        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1610
1611        if "stopOrders" in rawResponse.keys():
1612            rawStopOrders = rawResponse["stopOrders"]
1613            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1614
1615        else:
1616            rawStopOrders = []
1617            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1618
1619        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full', onlyFiles=False) -> dict:
1621    def Overview(self, show: bool = False, details: str = "full", onlyFiles=False) -> dict:
1622        """
1623        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1624        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1625        and `overviewBondsCalendarFile` are defined then also save information to file.
1626
1627        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1628        many requests about the state of the portfolio, and then, based on the received data, a large number
1629        of calculation and statistics are collected.
1630
1631        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1632        :param details: how detailed should the information be?
1633        - `full` — shows full available information about portfolio status (by default),
1634        - `positions` — shows only open positions,
1635        - `orders` — shows only sections of open limits and stop orders.
1636        - `digest` — show a short digest of the portfolio status,
1637        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1638        - `calendar` — shows only the bonds calendar section (if these present in portfolio).
1639        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
1640        :return: dictionary with client's raw portfolio and some statistics.
1641        """
1642        if self.accountId is None or not self.accountId:
1643            uLogger.error("Variable `accountId` must be defined for using this method!")
1644            raise Exception("Account ID required")
1645
1646        view = {
1647            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1648                "headers": {},  # list of dictionaries, response headers without "positions" section
1649                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1650                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1651                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1652                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1653                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1654                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1655                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1656                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1657                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1658            },
1659            "stat": {  # --- some statistics calculated using "raw" sections:
1660                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1661                "availableRUB": 0.,  # available rubles (without other currencies)
1662                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1663                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1664                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1665                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1666                "sharesCostRUB": 0.,  # costs of all shares in RUB
1667                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1668                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1669                "futuresCostRUB": 0.,  # costs of all futures in RUB
1670                "Currencies": [],  # list of dictionaries of all currencies statistics
1671                "Shares": [],  # list of dictionaries of all shares statistics
1672                "Bonds": [],  # list of dictionaries of all bonds statistics
1673                "Etfs": [],  # list of dictionaries of all etfs statistics
1674                "Futures": [],  # list of dictionaries of all futures statistics
1675                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1676                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1677                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1678                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1679                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1680            },
1681            "analytics": {  # --- some analytics of portfolio:
1682                "distrByAssets": {},  # portfolio distribution by assets
1683                "distrByCompanies": {},  # portfolio distribution by companies
1684                "distrBySectors": {},  # portfolio distribution by sectors
1685                "distrByCurrencies": {},  # portfolio distribution by currencies
1686                "distrByCountries": {},  # portfolio distribution by countries
1687                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1688            }
1689        }
1690
1691        details = details.lower()
1692        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1693        if details not in availableDetails:
1694            details = "full"
1695            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1696
1697        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1698
1699        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1700        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1701        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1702        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1703
1704        # save response headers without "positions" section:
1705        for key in portfolioResponse.keys():
1706            if key != "positions":
1707                view["raw"]["headers"][key] = portfolioResponse[key]
1708
1709            else:
1710                continue
1711
1712        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1713        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1714        for item in portfolioResponse["positions"]:
1715            if item["instrumentType"] == "currency":
1716                self._figi = item["figi"]
1717                if not self._figi and item["ticker"]:
1718                    self._ticker = item["ticker"]
1719                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1720
1721                curr = self.SearchByFIGI(requestPrice=False)
1722
1723                # current price of currency in RUB:
1724                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1725                    "name": curr["name"],
1726                    "currentPrice": NanoToFloat(
1727                        item["currentPrice"]["units"],
1728                        item["currentPrice"]["nano"]
1729                    ),
1730                }
1731
1732                view["raw"]["Currencies"].append(item)
1733
1734            elif item["instrumentType"] == "share":
1735                view["raw"]["Shares"].append(item)
1736
1737            elif item["instrumentType"] == "bond":
1738                view["raw"]["Bonds"].append(item)
1739
1740            elif item["instrumentType"] == "etf":
1741                view["raw"]["Etfs"].append(item)
1742
1743            elif item["instrumentType"] == "futures":
1744                view["raw"]["Futures"].append(item)
1745
1746            else:
1747                continue
1748
1749        # how many volume of currencies (by ISO currency name) are blocked:
1750        for item in view["raw"]["positions"]["blocked"]:
1751            blocked = NanoToFloat(item["units"], item["nano"])
1752            if blocked > 0:
1753                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1754
1755        # how many volume of instruments (by FIGI) are blocked:
1756        for item in view["raw"]["positions"]["securities"]:
1757            blocked = int(item["blocked"])
1758            if blocked > 0:
1759                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1760
1761        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1762
1763        if "rub" in allBlocked.keys():
1764            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1765
1766        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1767        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1768        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1769        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1770        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1771        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1772        view["stat"]["portfolioCostRUB"] = sum([
1773            view["stat"]["allCurrenciesCostRUB"],
1774            view["stat"]["sharesCostRUB"],
1775            view["stat"]["bondsCostRUB"],
1776            view["stat"]["etfsCostRUB"],
1777            view["stat"]["futuresCostRUB"],
1778        ])
1779
1780        # --- calculating some portfolio statistics:
1781        byComp = {}  # distribution by companies
1782        bySect = {}  # distribution by sectors
1783        byCurr = {}  # distribution by currencies (include RUB)
1784        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1785        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1786
1787        for item in portfolioResponse["positions"]:
1788            self._figi = item["figi"]
1789            if not self._figi and item["ticker"]:
1790                self._ticker = item["ticker"]
1791                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1792
1793            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1794
1795            if instrument:
1796                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1797                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1798
1799                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1800                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1801
1802                else:
1803                    blocked = 0
1804
1805                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1806                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1807                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1808                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1809                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1810                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1811                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1812                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1813                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1814                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1815                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1816                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1817
1818                statData = {
1819                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1820                    "ticker": instrument["ticker"],  # ticker by FIGI
1821                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1822                    "volume": volume,  # available volume of instrument
1823                    "lots": lots,  # volume in lots of instrument
1824                    "direction": direction,  # direction of an instrument's position: short or long
1825                    "blocked": blocked,  # blocked volume of currency or instrument
1826                    "currentPrice": curPrice,  # current instrument's price in basic asset
1827                    "average": average,  # current average position price
1828                    "cost": cost,  # current cost of all volume of instrument in basic asset
1829                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1830                    "costRUB": costRUB,  # cost of instrument in ruble
1831                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1832                    "profit": profit,  # expected profit at current moment
1833                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1834                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1835                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1836                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1837                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1838                    "step": instrument["step"],  # minimum price increment
1839                }
1840
1841                # adding distribution by unique countries:
1842                if statData["country"] not in byCountry.keys():
1843                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1844
1845                else:
1846                    byCountry[statData["country"]]["cost"] += costRUB
1847                    byCountry[statData["country"]]["percent"] += percentCostRUB
1848
1849                if item["instrumentType"] != "currency":
1850                    # adding distribution by unique companies:
1851                    if statData["name"]:
1852                        if statData["name"] not in byComp.keys():
1853                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1854
1855                        else:
1856                            byComp[statData["name"]]["cost"] += costRUB
1857                            byComp[statData["name"]]["percent"] += percentCostRUB
1858
1859                    # adding distribution by unique sectors:
1860                    if statData["sector"] not in bySect.keys():
1861                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1862
1863                    else:
1864                        bySect[statData["sector"]]["cost"] += costRUB
1865                        bySect[statData["sector"]]["percent"] += percentCostRUB
1866
1867                # adding distribution by unique currencies:
1868                if currency not in byCurr.keys():
1869                    byCurr[currency] = {
1870                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1871                        "cost": costRUB,
1872                        "percent": percentCostRUB
1873                    }
1874
1875                else:
1876                    byCurr[currency]["cost"] += costRUB
1877                    byCurr[currency]["percent"] += percentCostRUB
1878
1879                # saving statistics for every instrument:
1880                if item["instrumentType"] == "currency":
1881                    view["stat"]["Currencies"].append(statData)
1882
1883                    # update dict with free funds for trading (total - blocked) by currencies
1884                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1885                    view["stat"]["funds"][currency] = {
1886                        "total": volume,
1887                        "totalCostRUB": costRUB,  # total volume cost in rubles
1888                        "free": volume - blocked,
1889                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1890                    }
1891
1892                elif item["instrumentType"] == "share":
1893                    view["stat"]["Shares"].append(statData)
1894
1895                elif item["instrumentType"] == "bond":
1896                    view["stat"]["Bonds"].append(statData)
1897
1898                elif item["instrumentType"] == "etf":
1899                    view["stat"]["Etfs"].append(statData)
1900
1901                elif item["instrumentType"] == "Futures":
1902                    view["stat"]["Futures"].append(statData)
1903
1904                else:
1905                    continue
1906
1907        # total changes in Russian Ruble:
1908        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1909        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1910        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1911        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1912        view["stat"]["funds"]["rub"] = {
1913            "total": view["stat"]["availableRUB"],
1914            "totalCostRUB": view["stat"]["availableRUB"],
1915            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1916            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1917        }
1918
1919        # --- pending limit orders sector data:
1920        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1921        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1922
1923        for item in view["raw"]["orders"]:
1924            self._figi = item["figi"]
1925
1926            if item["figi"] not in uniquePendingOrdersFIGIs:
1927                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1928
1929                uniquePendingOrdersFIGIs.append(item["figi"])
1930                uniquePendingOrders[item["figi"]] = instrument
1931
1932            else:
1933                instrument = uniquePendingOrders[item["figi"]]
1934
1935            if instrument:
1936                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1937                orderType = TKS_ORDER_TYPES[item["orderType"]]
1938                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1939                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1940
1941                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1942                if item["direction"] == "ORDER_DIRECTION_BUY":
1943                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1944
1945                else:
1946                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1947
1948                # requested price for order execution:
1949                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1950
1951                # necessary changes in percent to reach target from current price:
1952                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1953
1954                view["stat"]["orders"].append({
1955                    "orderID": item["orderId"],  # orderId number parameter of current order
1956                    "figi": item["figi"],  # FIGI identification
1957                    "ticker": instrument["ticker"],  # ticker name by FIGI
1958                    "lotsRequested": item["lotsRequested"],  # requested lots value
1959                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1960                    "currentPrice": lastPrice,  # current instrument's price for defined action
1961                    "targetPrice": target,  # requested price for order execution in base currency
1962                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1963                    "percentChanges": changes,  # changes in percent to target from current price
1964                    "currency": item["currency"],  # instrument's currency name
1965                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1966                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1967                    "status": orderState,  # order status from TKS_ORDER_STATES
1968                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1969                })
1970
1971        # --- stop orders sector data:
1972        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1973        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1974
1975        for item in view["raw"]["stopOrders"]:
1976            self._figi = item["figi"]
1977
1978            if item["figi"] not in uniqueStopOrdersFIGIs:
1979                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1980
1981                uniqueStopOrdersFIGIs.append(item["figi"])
1982                uniqueStopOrders[item["figi"]] = instrument
1983
1984            else:
1985                instrument = uniqueStopOrders[item["figi"]]
1986
1987            if instrument:
1988                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1989                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1990                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1991
1992                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1993                if "expirationTime" in item.keys():
1994                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1995                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1996
1997                else:
1998                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1999                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
2000
2001                # current instrument's price (last sellers order if buy, and last buyers order if sell):
2002                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2003                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2004
2005                else:
2006                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2007
2008                # requested price when stop-order executed:
2009                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2010
2011                # price for limit-order, set up when stop-order executed:
2012                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2013
2014                # necessary changes in percent to reach target from current price:
2015                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2016
2017                view["stat"]["stopOrders"].append({
2018                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2019                    "figi": item["figi"],  # FIGI identification
2020                    "ticker": instrument["ticker"],  # ticker name by FIGI
2021                    "lotsRequested": item["lotsRequested"],  # requested lots value
2022                    "currentPrice": lastPrice,  # current instrument's price for defined action
2023                    "targetPrice": target,  # requested price for stop-order execution in base currency
2024                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2025                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2026                    "percentChanges": changes,  # changes in percent to target from current price
2027                    "currency": item["currency"],  # instrument's currency name
2028                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2029                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2030                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2031                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2032                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2033                })
2034
2035        # --- calculating data for analytics section:
2036        # portfolio distribution by assets:
2037        view["analytics"]["distrByAssets"] = {
2038            "Ruble": {
2039                "uniques": 1,
2040                "cost": view["stat"]["availableRUB"],
2041                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2042            },
2043            "Currencies": {
2044                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2045                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2046                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2047            },
2048            "Shares": {
2049                "uniques": len(view["stat"]["Shares"]),
2050                "cost": view["stat"]["sharesCostRUB"],
2051                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2052            },
2053            "Bonds": {
2054                "uniques": len(view["stat"]["Bonds"]),
2055                "cost": view["stat"]["bondsCostRUB"],
2056                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2057            },
2058            "Etfs": {
2059                "uniques": len(view["stat"]["Etfs"]),
2060                "cost": view["stat"]["etfsCostRUB"],
2061                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2062            },
2063            "Futures": {
2064                "uniques": len(view["stat"]["Futures"]),
2065                "cost": view["stat"]["futuresCostRUB"],
2066                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2067            },
2068        }
2069
2070        # portfolio distribution by companies:
2071        view["analytics"]["distrByCompanies"]["All money cash"] = {
2072            "ticker": "",
2073            "cost": view["stat"]["allCurrenciesCostRUB"],
2074            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2075        }
2076        view["analytics"]["distrByCompanies"].update(byComp)
2077
2078        # portfolio distribution by sectors:
2079        view["analytics"]["distrBySectors"]["All money cash"] = {
2080            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2081            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2082        }
2083        view["analytics"]["distrBySectors"].update(bySect)
2084
2085        # portfolio distribution by currencies:
2086        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2087            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2088
2089            if self.moreDebug:
2090                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2091
2092        view["analytics"]["distrByCurrencies"].update(byCurr)
2093        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2094        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2095
2096        # portfolio distribution by countries:
2097        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2098            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2099
2100            if self.moreDebug:
2101                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2102
2103        view["analytics"]["distrByCountries"].update(byCountry)
2104        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2105        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2106
2107        # --- Prepare text statistics overview in human-readable:
2108        if show or onlyFiles:
2109            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2110
2111            # Whatever the value `details`, header not changes:
2112            info = [
2113                "# Client's portfolio\n\n",
2114                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2115                "* **Account ID:** [{}]\n".format(self.accountId),
2116            ]
2117
2118            if details in ["full", "positions", "digest"]:
2119                info.extend([
2120                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2121                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2122                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2123                        view["stat"]["totalChangesRUB"],
2124                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2125                        view["stat"]["totalChangesPercentRUB"],
2126                    ),
2127                ])
2128
2129            if details in ["full", "positions"]:
2130                info.extend([
2131                    "## Open positions\n\n",
2132                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2133                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2134                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2135                        "{:.2f} ({:.2f}) rub".format(
2136                            view["stat"]["availableRUB"],
2137                            view["stat"]["blockedRUB"],
2138                        )
2139                    )
2140                ])
2141
2142                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2143                    return [
2144                        "|                             |                                 |          |              |              |                     |                              |\n",
2145                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2146                            noTradeStr if noTradeStr else typeStr,
2147                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2148                        ),
2149                    ]
2150
2151                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2152                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2153                        "{} [{}]".format(data["ticker"], data["figi"]),
2154                        "{:.2f} ({:.2f}) {}".format(
2155                            data["volume"],
2156                            data["blocked"],
2157                            data["currency"],
2158                        ) if isCurr else "{:.0f} ({:.0f})".format(
2159                            data["volume"],
2160                            data["blocked"],
2161                        ),
2162                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2163                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2164                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2165                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2166                        "{}{:.2f} {} ({}{:.2f}%)".format(
2167                            "+" if data["profit"] > 0 else "",
2168                            data["profit"], data["baseCurrencyName"],
2169                            "+" if data["percentProfit"] > 0 else "",
2170                            data["percentProfit"],
2171                        ),
2172                    )
2173
2174                # --- Show currencies section:
2175                if view["stat"]["Currencies"]:
2176                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2177                    for item in view["stat"]["Currencies"]:
2178                        info.append(_InfoStr(item, isCurr=True))
2179
2180                else:
2181                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2182
2183                # --- Show shares section:
2184                if view["stat"]["Shares"]:
2185                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2186
2187                    for item in view["stat"]["Shares"]:
2188                        info.append(_InfoStr(item))
2189
2190                else:
2191                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2192
2193                # --- Show bonds section:
2194                if view["stat"]["Bonds"]:
2195                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2196
2197                    for item in view["stat"]["Bonds"]:
2198                        info.append(_InfoStr(item))
2199
2200                else:
2201                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2202
2203                # --- Show etfs section:
2204                if view["stat"]["Etfs"]:
2205                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2206
2207                    for item in view["stat"]["Etfs"]:
2208                        info.append(_InfoStr(item))
2209
2210                else:
2211                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2212
2213                # --- Show futures section:
2214                if view["stat"]["Futures"]:
2215                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2216
2217                    for item in view["stat"]["Futures"]:
2218                        info.append(_InfoStr(item))
2219
2220                else:
2221                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2222
2223            if details in ["full", "orders"]:
2224                # --- Show pending limit orders section:
2225                if view["stat"]["orders"]:
2226                    info.extend([
2227                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2228                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2229                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2230                    ])
2231
2232                    for item in view["stat"]["orders"]:
2233                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2234                            "{} [{}]".format(item["ticker"], item["figi"]),
2235                            item["orderID"],
2236                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2237                            "{} {} ({}{:.2f}%)".format(
2238                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2239                                item["baseCurrencyName"],
2240                                "+" if item["percentChanges"] > 0 else "",
2241                                float(item["percentChanges"]),
2242                            ),
2243                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2244                            item["action"],
2245                            item["type"],
2246                            item["date"],
2247                        ))
2248
2249                else:
2250                    info.append("\n## Total pending limit-orders: [0]\n")
2251
2252                # --- Show stop orders section:
2253                if view["stat"]["stopOrders"]:
2254                    info.extend([
2255                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2256                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2257                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2258                    ])
2259
2260                    for item in view["stat"]["stopOrders"]:
2261                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2262                            "{} [{}]".format(item["ticker"], item["figi"]),
2263                            item["orderID"],
2264                            item["lotsRequested"],
2265                            "{} {} ({}{:.2f}%)".format(
2266                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2267                                item["baseCurrencyName"],
2268                                "+" if item["percentChanges"] > 0 else "",
2269                                float(item["percentChanges"]),
2270                            ),
2271                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2272                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2273                            item["action"],
2274                            item["type"],
2275                            item["expType"],
2276                            item["createDate"],
2277                            item["expDate"],
2278                        ))
2279
2280                else:
2281                    info.append("\n## Total stop-orders: [0]\n")
2282
2283            if details in ["full", "analytics"]:
2284                # -- Show analytics section:
2285                if view["stat"]["portfolioCostRUB"] > 0:
2286                    info.extend([
2287                        "\n# Analytics\n\n"
2288                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2289                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2290                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2291                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2292                            view["stat"]["totalChangesRUB"],
2293                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2294                            view["stat"]["totalChangesPercentRUB"],
2295                        ),
2296                        "\n## Portfolio distribution by assets\n"
2297                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2298                        "|------------------------------------|---------|---------|--------------------|\n",
2299                    ])
2300
2301                    for key in view["analytics"]["distrByAssets"].keys():
2302                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2303                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2304                                key,
2305                                view["analytics"]["distrByAssets"][key]["uniques"],
2306                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2307                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2308                            ))
2309
2310                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2311
2312                    info.extend([
2313                        "\n## Portfolio distribution by companies\n"
2314                        "\n| Company                                      | Percent | Current cost       |\n",
2315                        aSepLine,
2316                    ])
2317
2318                    for company in view["analytics"]["distrByCompanies"].keys():
2319                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2320                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2321                                "{}{}".format(
2322                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2323                                    company,
2324                                ),
2325                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2326                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2327                            ))
2328
2329                    info.extend([
2330                        "\n## Portfolio distribution by sectors\n"
2331                        "\n| Sector                                       | Percent | Current cost       |\n",
2332                        aSepLine,
2333                    ])
2334
2335                    for sector in view["analytics"]["distrBySectors"].keys():
2336                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2337                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2338                                sector,
2339                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2340                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2341                            ))
2342
2343                    info.extend([
2344                        "\n## Portfolio distribution by currencies\n"
2345                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2346                        aSepLine,
2347                    ])
2348
2349                    for curr in view["analytics"]["distrByCurrencies"].keys():
2350                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2351                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2352                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2353                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2354                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2355                            ))
2356
2357                    info.extend([
2358                        "\n## Portfolio distribution by countries\n"
2359                        "\n| Assets by country                            | Percent | Current cost       |\n",
2360                        aSepLine,
2361                    ])
2362
2363                    for country in view["analytics"]["distrByCountries"].keys():
2364                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2365                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2366                                country,
2367                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2368                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2369                            ))
2370
2371            if details in ["full", "calendar"]:
2372                # -- Show bonds payment calendar section:
2373                if view["stat"]["Bonds"]:
2374                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2375                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2376                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2377
2378                else:
2379                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2380
2381            infoText = "".join(info)
2382
2383            if show and not onlyFiles:
2384                uLogger.info(infoText)
2385
2386            if details == "full" and self.overviewFile:
2387                filename = self.overviewFile
2388
2389            elif details == "digest" and self.overviewDigestFile:
2390                filename = self.overviewDigestFile
2391
2392            elif details == "positions" and self.overviewPositionsFile:
2393                filename = self.overviewPositionsFile
2394
2395            elif details == "orders" and self.overviewOrdersFile:
2396                filename = self.overviewOrdersFile
2397
2398            elif details == "analytics" and self.overviewAnalyticsFile:
2399                filename = self.overviewAnalyticsFile
2400
2401            elif details == "calendar" and self.overviewBondsCalendarFile:
2402                filename = self.overviewBondsCalendarFile
2403
2404            else:
2405                filename = ""
2406
2407            if filename and (show or onlyFiles):
2408                with open(filename, "w", encoding="UTF-8") as fH:
2409                    fH.write(infoText)
2410
2411                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2412
2413                if self.useHTMLReports:
2414                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2415                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2416                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2417
2418                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2419
2420        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio).
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2422    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True, onlyFiles=False) -> tuple[list[dict], dict]:
2423        """
2424        Returns history operations between two given dates for current `accountId`.
2425        If `reportFile` string is not empty then also save human-readable report.
2426        Shows some statistical data of closed positions.
2427
2428        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2429        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2430        :param show: if `True` then also prints all records to the console.
2431        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2432        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2433        :return: original list of dictionaries with history of deals records from API ("operations" key):
2434                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2435                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2436        """
2437        if self.accountId is None or not self.accountId:
2438            uLogger.error("Variable `accountId` must be defined for using this method!")
2439            raise Exception("Account ID required")
2440
2441        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2442
2443        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2444
2445        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2446        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2447        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2448        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2449        customStat = {}  # custom statistics in additional to responseJSON
2450
2451        # --- output report in human-readable format:
2452        if self.reportFile and (show or onlyFiles):
2453            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2454            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2455            nextDay = ""
2456
2457            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2458
2459            if len(ops) > 0:
2460                customStat = {
2461                    "opsCount": 0,  # total operations count
2462                    "buyCount": 0,  # buy operations
2463                    "sellCount": 0,  # sell operations
2464                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2465                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2466                    "payIn": {"rub": 0.},  # Deposit brokerage account
2467                    "payOut": {"rub": 0.},  # Withdrawals
2468                    "divs": {"rub": 0.},  # Dividends income
2469                    "coupons": {"rub": 0.},  # Coupon's income
2470                    "brokerCom": {"rub": 0.},  # Service commissions
2471                    "serviceCom": {"rub": 0.},  # Service commissions
2472                    "marginCom": {"rub": 0.},  # Margin commissions
2473                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2474                }
2475
2476                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2477                for item in ops:
2478                    if item["state"] == "OPERATION_STATE_EXECUTED":
2479                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2480
2481                        # count buy operations:
2482                        if "_BUY" in item["operationType"]:
2483                            customStat["buyCount"] += 1
2484
2485                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2486                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2487
2488                            else:
2489                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2490
2491                        # count sell operations:
2492                        elif "_SELL" in item["operationType"]:
2493                            customStat["sellCount"] += 1
2494
2495                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2496                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2497
2498                            else:
2499                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2500
2501                        # count incoming operations:
2502                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2503                            if item["payment"]["currency"] in customStat["payIn"].keys():
2504                                customStat["payIn"][item["payment"]["currency"]] += payment
2505
2506                            else:
2507                                customStat["payIn"][item["payment"]["currency"]] = payment
2508
2509                        # count withdrawals operations:
2510                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2511                            if item["payment"]["currency"] in customStat["payOut"].keys():
2512                                customStat["payOut"][item["payment"]["currency"]] += payment
2513
2514                            else:
2515                                customStat["payOut"][item["payment"]["currency"]] = payment
2516
2517                        # count dividends income:
2518                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2519                            if item["payment"]["currency"] in customStat["divs"].keys():
2520                                customStat["divs"][item["payment"]["currency"]] += payment
2521
2522                            else:
2523                                customStat["divs"][item["payment"]["currency"]] = payment
2524
2525                        # count coupon's income:
2526                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2527                            if item["payment"]["currency"] in customStat["coupons"].keys():
2528                                customStat["coupons"][item["payment"]["currency"]] += payment
2529
2530                            else:
2531                                customStat["coupons"][item["payment"]["currency"]] = payment
2532
2533                        # count broker commissions:
2534                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2535                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2536                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2537
2538                            else:
2539                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2540
2541                        # count service commissions:
2542                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2543                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2544                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2545
2546                            else:
2547                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2548
2549                        # count margin commissions:
2550                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2551                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2552                                customStat["marginCom"][item["payment"]["currency"]] += payment
2553
2554                            else:
2555                                customStat["marginCom"][item["payment"]["currency"]] = payment
2556
2557                        # count withholding taxes:
2558                        elif "_TAX" in item["operationType"]:
2559                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2560                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2561
2562                            else:
2563                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2564
2565                        else:
2566                            continue
2567
2568                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2569
2570                # --- view "Actions" lines:
2571                info.extend([
2572                    "| Report sections            |                               |                              |                      |                        |\n",
2573                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2574                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2575                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2576                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2577                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2578                    ),
2579                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2580                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2581                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2582                    ),
2583                ])
2584
2585                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2586                for key in opsKeys:
2587                    if key == "rub":
2588                        continue
2589
2590                    info.extend([
2591                        "|                            |                               | {:<28} |                      |                        |\n".format(
2592                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2593                        ),
2594                        "|                            |                               | {:<28} |                      |                        |\n".format(
2595                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2596                        ),
2597                    ])
2598
2599                info.append(splitLine1)
2600
2601                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2602                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2603                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2604                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2605                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2606                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2607                    )
2608
2609                # --- view "Payments" lines:
2610                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2611                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2612
2613                for key in paymentsKeys:
2614                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2615
2616                info.append(splitLine1)
2617
2618                # --- view "Commissions and taxes" lines:
2619                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2620                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2621
2622                for key in comKeys:
2623                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2624
2625                info.extend([
2626                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2627                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2628                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2629                ])
2630
2631            else:
2632                info.append("Broker returned no operations during this period\n")
2633
2634            # --- view "Operations" section:
2635            for item in ops:
2636                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2637                    continue
2638
2639                else:
2640                    self._figi = item["figi"]
2641                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2642                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2643
2644                    # group of deals during one day:
2645                    if nextDay and item["date"].split("T")[0] != nextDay:
2646                        info.append(splitLine2)
2647                        nextDay = ""
2648
2649                    else:
2650                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2651
2652                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2653                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2654                        self._figi if self._figi else "—",
2655                        instrument["ticker"] if instrument else "—",
2656                        instrument["type"] if instrument else "—",
2657                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2658                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2659                        TKS_OPERATION_STATES[item["state"]],
2660                        TKS_OPERATION_TYPES[item["operationType"]],
2661                    ))
2662
2663            infoText = "".join(info)
2664
2665            if show and not onlyFiles:
2666                if self.moreDebug:
2667                    uLogger.debug("Records about history of a client's operations successfully received")
2668
2669                uLogger.info(infoText)
2670
2671            if self.reportFile and (show or onlyFiles):
2672                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2673                    fH.write(infoText)
2674
2675                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2676
2677                if self.useHTMLReports:
2678                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2679                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2680                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2681
2682                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2683
2684        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False, onlyFiles=False) -> pandas.core.frame.DataFrame:
2686    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False, onlyFiles=False) -> pd.DataFrame:
2687        """
2688        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2689
2690        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2691        Warning! Broker server used ISO UTC time by default.
2692
2693        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2694        Also, `historyFile` used to update history with `onlyMissing` parameter.
2695
2696        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2697
2698        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2699        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2700        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2701                         `"hour"`, `"day"`. Default: `"hour"`.
2702        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2703                            False by default. Warning! History appends only from last candle to current time
2704                            with always update last candle!
2705        :param csvSep: separator if csv-file is used, `,` by default.
2706        :param show: if `True` then also prints Pandas DataFrame to the console.
2707        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
2708        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2709                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2710        """
2711        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2712        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2713        history = None  # empty pandas object for history
2714
2715        if interval not in TKS_CANDLE_INTERVALS.keys():
2716            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2717            raise Exception("Incorrect value")
2718
2719        if not (self._ticker or self._figi):
2720            uLogger.error("Ticker or FIGI must be defined!")
2721            raise Exception("Ticker or FIGI required")
2722
2723        if self._ticker and not self._figi:
2724            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2725            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2726
2727        if self._figi and not self._ticker:
2728            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2729            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2730
2731        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2732        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2733        if interval.lower() != "day":
2734            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2735
2736        delta = dtEnd - dtStart  # current UTC time minus last time in file
2737        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2738
2739        # calculate history length in candles:
2740        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2741        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2742            length += 1  # to avoid fraction time
2743
2744        # calculate data blocks count:
2745        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2746
2747        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2748        if self.moreDebug:
2749            uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2750            uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2751            uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2752            uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2753
2754        tempOld = None  # pandas object for old history, if --only-missing key present
2755        lastTime = None  # datetime object of last old candle in file
2756
2757        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2758            if self.moreDebug:
2759                uLogger.debug("--only-missing key present, add only last missing candles...")
2760                uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2761
2762            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2763
2764            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2765            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2766            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2767            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2768
2769            # get last datetime object from last string in file or minus 1 delta if file is empty:
2770            if len(tempOld) > 0:
2771                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2772
2773            else:
2774                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2775
2776            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2777
2778        responseJSONs = []  # raw history blocks of data
2779
2780        blockEnd = dtEnd
2781        for item in range(blocks):
2782            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2783            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2784
2785            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2786                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2787            ))
2788
2789            if blockStart == blockEnd:
2790                uLogger.debug("Skipped this zero-length block...")
2791
2792            else:
2793                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2794                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2795                self.body = str({
2796                    "figi": self._figi,
2797                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2798                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2799                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2800                })
2801                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2802
2803                if "code" in responseJSON.keys():
2804                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2805
2806                else:
2807                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2808                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2809
2810                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2811
2812            blockEnd = blockStart
2813
2814        printCount = len(responseJSONs)  # candles to show in console
2815        if responseJSONs:
2816            tempHistory = pd.DataFrame(
2817                data={
2818                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2819                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2820                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2821                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2822                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2823                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2824                    "volume": [int(item["volume"]) for item in responseJSONs],
2825                },
2826                index=range(len(responseJSONs)),
2827                columns=["date", "time", "open", "high", "low", "close", "volume"],
2828            )
2829            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2830            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2831
2832            # append only newest candles to old history if --only-missing key present:
2833            if onlyMissing and tempOld is not None and lastTime is not None:
2834                index = 0  # find start index in tempHistory data:
2835
2836                for i, item in tempHistory.iterrows():
2837                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2838
2839                    if curTime == lastTime:
2840                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2841                        index = i
2842                        printCount = index + 1
2843                        break
2844
2845                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2846
2847            else:
2848                history = tempHistory  # if no `--only-missing` key then load full data from server
2849
2850            if self.moreDebug:
2851                uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2852
2853        if history is not None and not history.empty:
2854            if show and not onlyFiles:
2855                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2856                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2857                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2858                ))
2859
2860        else:
2861            uLogger.warning("Received an empty candles history!")
2862
2863        if self.historyFile is not None:
2864            if history is not None and not history.empty:
2865                history.to_csv(self.historyFile, sep=csvSep, index=False, header=False)
2866                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2867
2868            else:
2869                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2870
2871        else:
2872            if self.moreDebug:
2873                uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2874
2875        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2877    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2878        """
2879        Load candles history from csv-file and return Pandas DataFrame object.
2880
2881        See also: `History()` and `ShowHistoryChart()` methods.
2882
2883        :param filePath: path to csv-file to open.
2884        """
2885        loadedHistory = None  # init candles data object
2886
2887        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2888
2889        if os.path.exists(filePath):
2890            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2891
2892            tfStr = self.priceModel.FormattedDelta(
2893                self.priceModel.timeframe,
2894                "{days} days {hours}h {minutes}m {seconds}s",
2895            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2896                self.priceModel.timeframe,
2897                "{hours}h {minutes}m {seconds}s",
2898            )
2899
2900            if loadedHistory is not None and not loadedHistory.empty:
2901                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2902                    len(loadedHistory),
2903                    tfStr,
2904                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2905                )
2906
2907            else:
2908                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2909
2910        else:
2911            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2912
2913        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2915    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2916        """
2917        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2918
2919        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2920        Default: `index.html` (both for interact and non-interact candlesticks chart).
2921
2922        See also: `History()` and `LoadHistory()` methods.
2923
2924        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2925        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2926                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2927                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2928                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2929        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2930                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2931        """
2932        if isinstance(candles, str):
2933            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2934            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2935
2936        elif isinstance(candles, pd.DataFrame):
2937            self.priceModel.prices = candles  # set candles chain from variable
2938            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2939
2940            if "datetime" not in candles.columns:
2941                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2942
2943        else:
2944            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2945            raise Exception("Incorrect value")
2946
2947        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2948
2949        if interact:
2950            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2951
2952            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2953
2954        else:
2955            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2956
2957            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2958
2959        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2961    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2962        """
2963        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2964        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2965
2966        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2967
2968        :param operation: string "Buy" or "Sell".
2969        :param lots: volume, integer count of lots >= 1.
2970        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2971        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2972        :param expDate: string "Undefined" by default or local date in future,
2973                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2974        :return: JSON with response from broker server.
2975        """
2976        if self.accountId is None or not self.accountId:
2977            uLogger.error("Variable `accountId` must be defined for using this method!")
2978            raise Exception("Account ID required")
2979
2980        if operation is None or not operation or operation not in ("Buy", "Sell"):
2981            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2982            raise Exception("Incorrect value")
2983
2984        if lots is None or lots < 1:
2985            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2986            lots = 1
2987
2988        if tp is None or tp < 0:
2989            tp = 0
2990
2991        if sl is None or sl < 0:
2992            sl = 0
2993
2994        if expDate is None or not expDate:
2995            expDate = "Undefined"
2996
2997        if not (self._ticker or self._figi):
2998            uLogger.error("Ticker or FIGI must be defined!")
2999            raise Exception("Ticker or FIGI required")
3000
3001        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3002        self._ticker = instrument["ticker"]
3003        self._figi = instrument["figi"]
3004
3005        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
3006
3007        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3008        self.body = str({
3009            "figi": self._figi,
3010            "quantity": str(lots),
3011            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3012            "accountId": str(self.accountId),
3013            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3014        })
3015        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3016
3017        if "orderId" in response.keys():
3018            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3019                operation, response["orderId"],
3020                self._ticker, self._figi, lots,
3021                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3022                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3023                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3024            ))
3025
3026            if tp > 0:
3027                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3028
3029            if sl > 0:
3030                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3031
3032        else:
3033            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3034
3035        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3037    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3038        """
3039        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3040        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3041
3042        See also: `Order()` and `Trade()` docstrings.
3043
3044        :param lots: volume, integer count of lots >= 1.
3045        :param tp: float > 0, take profit price of stop-order.
3046        :param sl: float > 0, stop loss price of stop-order.
3047        :param expDate: it's a local date in future.
3048                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3049        :return: JSON with response from broker server.
3050        """
3051        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3053    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3054        """
3055        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3056        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3057
3058        See also: `Order()` and `Trade()` docstrings.
3059
3060        :param lots: volume, integer count of lots >= 1.
3061        :param tp: float > 0, take profit price of stop-order.
3062        :param sl: float > 0, stop loss price of stop-order.
3063        :param expDate: it's a local date in the future.
3064                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3065        :return: JSON with response from broker server.
3066        """
3067        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3069    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3070        """
3071        Close position of given instruments.
3072
3073        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3074        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3075                         This avoids unnecessary downloading data from the server.
3076        """
3077        if instruments is None or not instruments:
3078            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3079            raise Exception("Ticker or FIGI required")
3080
3081        if isinstance(instruments, str):
3082            instruments = [instruments]
3083
3084        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3085        if uniqueInstruments:
3086            if portfolio is None or not portfolio:
3087                portfolio = self.Overview(show=False)
3088
3089            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3090            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3091
3092            for self._figi in uniqueInstruments:
3093                if self._figi not in allOpened:
3094                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3095                    continue
3096
3097                # search open trade info about instrument by ticker:
3098                instrument = {}
3099                for iType in TKS_INSTRUMENTS:
3100                    if instrument:
3101                        break
3102
3103                    for item in portfolio["stat"][iType]:
3104                        if item["figi"] == self._figi:
3105                            instrument = item
3106                            break
3107
3108                if instrument:
3109                    self._ticker = instrument["ticker"]
3110                    self._figi = instrument["figi"]
3111
3112                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3113                        self._ticker,
3114                        self._figi,
3115                        int(instrument["volume"]),
3116                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3117                    ))
3118
3119                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3120
3121                    if tradeLots > 0:
3122                        if instrument["blocked"] > 0:
3123                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3124                                instrument["blocked"],
3125                                self._ticker,
3126                                tradeLots,
3127                            ))
3128
3129                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3130                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3131
3132                    else:
3133                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3135    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3136        """
3137        Close all positions of given instruments with defined type.
3138
3139        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3140        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3141                         This avoids unnecessary downloading data from the server.
3142        """
3143        if iType not in TKS_INSTRUMENTS:
3144            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3145
3146        else:
3147            if portfolio is None or not portfolio:
3148                portfolio = self.Overview(show=False)
3149
3150            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3151            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3152
3153            if tickers and portfolio:
3154                self.CloseTrades(tickers, portfolio)
3155
3156            else:
3157                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3159    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3160        """
3161        Universal method to create market or limit orders with all available parameters for current `accountId`.
3162        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3163
3164        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3165        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3166
3167        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3168        then broker immediately open market order as you can do simple --buy or --sell operations!
3169
3170        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3171        When current price will go up or down to target price value then broker opens a limit order.
3172        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3173
3174        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3175
3176        :param operation: string "Buy" or "Sell".
3177        :param orderType: string "Limit" or "Stop".
3178        :param lots: volume, integer count of lots >= 1.
3179        :param targetPrice: target price > 0. This is open trade price for limit order.
3180        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3181                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3182        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3183                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3184                         Stop loss order always executed by market price.
3185        :param expDate: string "Undefined" by default or local date in future.
3186                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3187                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3188                        A limit order has no expiration date, it lasts until the end of the trading day.
3189        :return: JSON with response from broker server.
3190        """
3191        if self.accountId is None or not self.accountId:
3192            uLogger.error("Variable `accountId` must be defined for using this method!")
3193            raise Exception("Account ID required")
3194
3195        if operation is None or not operation or operation not in ("Buy", "Sell"):
3196            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3197            raise Exception("Incorrect value")
3198
3199        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3200            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3201            raise Exception("Incorrect value")
3202
3203        if lots is None or lots < 1:
3204            uLogger.error("You must define trade volume > 0: integer count of lots!")
3205            raise Exception("Incorrect value")
3206
3207        if targetPrice is None or targetPrice <= 0:
3208            uLogger.error("Target price for limit-order must be greater than 0!")
3209            raise Exception("Incorrect value")
3210
3211        if limitPrice is None or limitPrice <= 0:
3212            limitPrice = targetPrice
3213
3214        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3215            stopType = "Limit"
3216
3217        if expDate is None or not expDate:
3218            expDate = "Undefined"
3219
3220        if not (self._ticker or self._figi):
3221            uLogger.error("Tocker or FIGI must be defined!")
3222            raise Exception("Ticker or FIGI required")
3223
3224        response = {}
3225        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3226        self._ticker = instrument["ticker"]
3227        self._figi = instrument["figi"]
3228
3229        if orderType == "Limit":
3230            uLogger.debug(
3231                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3232                    self._ticker, self._figi,
3233                    operation, lots, targetPrice, instrument["currency"],
3234                ))
3235
3236            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3237            self.body = str({
3238                "figi": self._figi,
3239                "quantity": str(lots),
3240                "price": FloatToNano(targetPrice),
3241                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3242                "accountId": str(self.accountId),
3243                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3244            })
3245            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3246
3247            if "orderId" in response.keys():
3248                uLogger.info(
3249                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3250                        response["orderId"], self._ticker, self._figi, operation, lots,
3251                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3252                    ))
3253
3254                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3255                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3256                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3257                            targetPrice, instrument["currency"],
3258                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3259                        ))
3260
3261                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3262                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3263                            targetPrice, instrument["currency"],
3264                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3265                        ))
3266
3267            else:
3268                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3269
3270        if orderType == "Stop":
3271            uLogger.debug(
3272                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3273                    self._ticker, self._figi,
3274                    operation, lots,
3275                    targetPrice, instrument["currency"],
3276                    limitPrice, instrument["currency"],
3277                    stopType, expDate,
3278                ))
3279
3280            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3281            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3282            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3283
3284            body = {
3285                "figi": self._figi,
3286                "quantity": str(lots),
3287                "price": FloatToNano(limitPrice),
3288                "stopPrice": FloatToNano(targetPrice),
3289                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3290                "accountId": str(self.accountId),
3291                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3292                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3293            }
3294
3295            if expDateUTC:
3296                body["expireDate"] = expDateUTC
3297
3298            self.body = str(body)
3299            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3300
3301            if "stopOrderId" in response.keys():
3302                uLogger.info(
3303                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3304                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3305                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3306                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3307                        TKS_STOP_ORDER_TYPES[stopOrderType],
3308                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3309                    ))
3310
3311                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3312                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3313                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3314                            targetPrice, instrument["currency"],
3315                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3316                        ))
3317
3318                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3319                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3320                            targetPrice, instrument["currency"],
3321                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3322                        ))
3323
3324            else:
3325                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3326
3327        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3329    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3330        """
3331        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3332        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3333        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3334        See also: `Order()` docstring.
3335
3336        :param lots: volume, integer count of lots >= 1.
3337        :param targetPrice: target price > 0. This is open trade price for limit order.
3338        :return: JSON with response from broker server.
3339        """
3340        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3342    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3343        """
3344        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3345        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3346        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3347        target price value then broker opens a limit order. See also: `Order()` docstring.
3348
3349        :param lots: volume, integer count of lots >= 1.
3350        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3351        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3352                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3353        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3354                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3355        :param expDate: string "Undefined" by default or local date in future.
3356                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3357                        This date is converting to UTC format for server.
3358        :return: JSON with response from broker server.
3359        """
3360        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3362    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3363        """
3364        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3365        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3366        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3367        See also: `Order()` docstring.
3368
3369        :param lots: volume, integer count of lots >= 1.
3370        :param targetPrice: target price > 0. This is open trade price for limit order.
3371        :return: JSON with response from broker server.
3372        """
3373        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3375    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3376        """
3377        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3378        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3379        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3380        target price value then broker opens a limit order. See also: `Order()` docstring.
3381
3382        :param lots: volume, integer count of lots >= 1.
3383        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3384        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3385                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3386        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3387                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3388        :param expDate: string "Undefined" by default or local date in future.
3389                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3390                        This date is converting to UTC format for server.
3391        :return: JSON with response from broker server.
3392        """
3393        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3395    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3396        """
3397        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3398
3399        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3400        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3401                             This avoids unnecessary downloading data from the server.
3402        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3403        """
3404        if self.accountId is None or not self.accountId:
3405            uLogger.error("Variable `accountId` must be defined for using this method!")
3406            raise Exception("Account ID required")
3407
3408        if orderIDs:
3409            if allOrdersIDs is None:
3410                rawOrders = self.RequestPendingOrders()
3411                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3412
3413            if allStopOrdersIDs is None:
3414                rawStopOrders = self.RequestStopOrders()
3415                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3416
3417            for orderID in orderIDs:
3418                idInPendingOrders = orderID in allOrdersIDs
3419                idInStopOrders = orderID in allStopOrdersIDs
3420
3421                if not (idInPendingOrders or idInStopOrders):
3422                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3423                    continue
3424
3425                else:
3426                    if idInPendingOrders:
3427                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3428
3429                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3430                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3431                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3432                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3433
3434                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3435                            if self.moreDebug:
3436                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3437
3438                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3439
3440                        else:
3441                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3442
3443                    elif idInStopOrders:
3444                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3445
3446                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3447                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3448                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3449                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3450
3451                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3452                            if self.moreDebug:
3453                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3454
3455                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3456
3457                        else:
3458                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3459
3460                    else:
3461                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3463    def CloseAllOrders(self) -> None:
3464        """
3465        Gets a list of open pending and stop orders and cancel it all.
3466        """
3467        rawOrders = self.RequestPendingOrders()
3468        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3469        lenOrders = len(allOrdersIDs)
3470
3471        rawStopOrders = self.RequestStopOrders()
3472        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3473        lenSOrders = len(allStopOrdersIDs)
3474
3475        if lenOrders > 0 or lenSOrders > 0:
3476            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3477
3478            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3479
3480        else:
3481            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3483    def CloseAll(self, *args) -> None:
3484        """
3485        Close all available (not blocked) opened trades and orders.
3486
3487        Also, you can select one or more keywords case-insensitive:
3488        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3489
3490        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3491        """
3492        overview = self.Overview(show=False)  # get all open trades info
3493
3494        if len(args) == 0:
3495            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3496            self.CloseAllOrders()  # close all pending and stop orders
3497
3498            for iType in TKS_INSTRUMENTS:
3499                if iType != "Currencies":
3500                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3501
3502        else:
3503            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3504            lowerArgs = [x.lower() for x in args]
3505
3506            if "orders" in lowerArgs:
3507                self.CloseAllOrders()  # close all pending and stop orders
3508
3509            for iType in TKS_INSTRUMENTS:
3510                if iType.lower() in lowerArgs and iType != "Currencies":
3511                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3513    def CloseAllByTicker(self, instrument: str) -> None:
3514        """
3515        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3516
3517        This method searches opened trade and orders of instrument throw all portfolio and then use
3518        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3519
3520        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3521
3522        :param instrument: string with ticker.
3523        """
3524        if instrument is None or not instrument:
3525            uLogger.error("Ticker name must be defined for using this method!")
3526            raise Exception("Ticker required")
3527
3528        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3529
3530        self._ticker = instrument  # try to set instrument as ticker
3531        self._figi = ""
3532
3533        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3534        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3535
3536        if limitAll and self.IsInLimitOrders(portfolio=overview):
3537            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3538            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3539
3540        if stopAll and self.IsInStopOrders(portfolio=overview):
3541            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3542            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3543
3544        if self.IsInPortfolio(portfolio=overview):
3545            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3546            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3548    def CloseAllByFIGI(self, instrument: str) -> None:
3549        """
3550        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3551
3552        This method searches opened trade and orders of instrument throw all portfolio and then use
3553        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3554
3555        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3556
3557        :param instrument: string with FIGI id.
3558        """
3559        if instrument is None or not instrument:
3560            uLogger.error("FIGI id must be defined for using this method!")
3561            raise Exception("FIGI required")
3562
3563        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3564
3565        self._ticker = ""
3566        self._figi = instrument  # try to set instrument as FIGI id
3567
3568        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3569        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3570
3571        if limitAll and self.IsInLimitOrders(portfolio=overview):
3572            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3573            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3574
3575        if stopAll and self.IsInStopOrders(portfolio=overview):
3576            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3577            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3578
3579        if self.IsInPortfolio(portfolio=overview):
3580            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3581            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters):
3583    @staticmethod
3584    def ParseOrderParameters(operation, **inputParameters):
3585        """
3586        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3587
3588        :param operation: string "Buy" or "Sell".
3589        :param inputParameters: this is dict of strings that looks like this
3590               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3591               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3592               "prices" key: one or more prices to open limit-orders
3593               Counts of values in lots and prices lists must be equals!
3594        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3595        """
3596        # TODO: update order grid work with api v2
3597        pass
3598        # uLogger.debug("Input parameters: {}".format(inputParameters))
3599        #
3600        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3601        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3602        #     raise Exception("Incorrect value")
3603        #
3604        # if "l" in inputParameters.keys():
3605        #     inputParameters["lots"] = inputParameters.pop("l")
3606        #
3607        # if "p" in inputParameters.keys():
3608        #     inputParameters["prices"] = inputParameters.pop("p")
3609        #
3610        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3611        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3612        #     raise Exception("Incorrect value")
3613        #
3614        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3615        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3616        #
3617        # if len(lots) != len(prices):
3618        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3619        #     raise Exception("Incorrect value")
3620        #
3621        # uLogger.debug("Extracted parameters for orders:")
3622        # uLogger.debug("lots = {}".format(lots))
3623        # uLogger.debug("prices = {}".format(prices))
3624        #
3625        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3626        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3627        # uLogger.debug("Order parameters: {}".format(result))
3628        #
3629        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3631    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3632        """
3633        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3634
3635        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3636        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3637        """
3638        result = False
3639        msg = "Instrument not defined!"
3640
3641        if portfolio is None or not portfolio:
3642            portfolio = self.Overview(show=False)
3643
3644        if self._ticker:
3645            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3646            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3647
3648            for iType in TKS_INSTRUMENTS:
3649                for instrument in portfolio["stat"][iType]:
3650                    if instrument["ticker"] == self._ticker:
3651                        result = True
3652                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3653                        break
3654
3655        elif self._figi:
3656            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3657            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3658
3659            for iType in TKS_INSTRUMENTS:
3660                for instrument in portfolio["stat"][iType]:
3661                    if instrument["figi"] == self._figi:
3662                        result = True
3663                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3664                        break
3665
3666        else:
3667            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3668
3669        uLogger.debug(msg)
3670
3671        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3673    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3674        """
3675        Returns instrument from the user's portfolio if it presents there.
3676        Instrument must be defined by `ticker` (highly priority) or `figi`.
3677
3678        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3679        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3680        """
3681        result = None
3682        msg = "Instrument not defined!"
3683
3684        if portfolio is None or not portfolio:
3685            portfolio = self.Overview(show=False)
3686
3687        if self._ticker:
3688            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3689            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3690
3691            for iType in TKS_INSTRUMENTS:
3692                for instrument in portfolio["stat"][iType]:
3693                    if instrument["ticker"] == self._ticker:
3694                        result = instrument
3695                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3696                        break
3697
3698        elif self._figi:
3699            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3700            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3701
3702            for iType in TKS_INSTRUMENTS:
3703                for instrument in portfolio["stat"][iType]:
3704                    if instrument["figi"] == self._figi:
3705                        result = instrument
3706                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3707                        break
3708
3709        else:
3710            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3711
3712        uLogger.debug(msg)
3713
3714        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3716    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3717        """
3718        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3719
3720        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3721
3722        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3723        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3724        """
3725        result = False
3726        msg = "Instrument not defined!"
3727
3728        if portfolio is None or not portfolio:
3729            portfolio = self.Overview(show=False)
3730
3731        if self._ticker:
3732            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3733            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3734
3735            for instrument in portfolio["stat"]["orders"]:
3736                if instrument["ticker"] == self._ticker:
3737                    result = True
3738                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3739                    break
3740
3741        elif self._figi:
3742            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3743            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3744
3745            for instrument in portfolio["stat"]["orders"]:
3746                if instrument["figi"] == self._figi:
3747                    result = True
3748                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3749                    break
3750
3751        else:
3752            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3753
3754        uLogger.debug(msg)
3755
3756        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3758    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3759        """
3760        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3761        Instrument must be defined by `ticker` (highly priority) or `figi`.
3762
3763        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3764
3765        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3766        :return: list with `orderID`s of limit orders.
3767        """
3768        result = []
3769        msg = "Instrument not defined!"
3770
3771        if portfolio is None or not portfolio:
3772            portfolio = self.Overview(show=False)
3773
3774        if self._ticker:
3775            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3776            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3777
3778            for instrument in portfolio["stat"]["orders"]:
3779                if instrument["ticker"] == self._ticker:
3780                    result.append(instrument["orderID"])
3781
3782            if result:
3783                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3784
3785        elif self._figi:
3786            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3787            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3788
3789            for instrument in portfolio["stat"]["orders"]:
3790                if instrument["figi"] == self._figi:
3791                    result.append(instrument["orderID"])
3792
3793            if result:
3794                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3795
3796        else:
3797            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3798
3799        uLogger.debug(msg)
3800
3801        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3803    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3804        """
3805        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3806
3807        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3808
3809        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3810        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3811        """
3812        result = False
3813        msg = "Instrument not defined!"
3814
3815        if portfolio is None or not portfolio:
3816            portfolio = self.Overview(show=False)
3817
3818        if self._ticker:
3819            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3820            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3821
3822            for instrument in portfolio["stat"]["stopOrders"]:
3823                if instrument["ticker"] == self._ticker:
3824                    result = True
3825                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3826                    break
3827
3828        elif self._figi:
3829            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3830            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3831
3832            for instrument in portfolio["stat"]["stopOrders"]:
3833                if instrument["figi"] == self._figi:
3834                    result = True
3835                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3836                    break
3837
3838        else:
3839            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3840
3841        uLogger.debug(msg)
3842
3843        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3845    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3846        """
3847        Returns list with all `orderID`s of opened stop orders for the instrument.
3848        Instrument must be defined by `ticker` (highly priority) or `figi`.
3849
3850        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3851
3852        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3853        :return: list with `orderID`s of stop orders.
3854        """
3855        result = []
3856        msg = "Instrument not defined!"
3857
3858        if portfolio is None or not portfolio:
3859            portfolio = self.Overview(show=False)
3860
3861        if self._ticker:
3862            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3863            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3864
3865            for instrument in portfolio["stat"]["stopOrders"]:
3866                if instrument["ticker"] == self._ticker:
3867                    result.append(instrument["orderID"])
3868
3869            if result:
3870                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3871
3872        elif self._figi:
3873            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3874            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3875
3876            for instrument in portfolio["stat"]["stopOrders"]:
3877                if instrument["figi"] == self._figi:
3878                    result.append(instrument["orderID"])
3879
3880            if result:
3881                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3882
3883        else:
3884            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3885
3886        uLogger.debug(msg)
3887
3888        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3890    def RequestLimits(self) -> dict:
3891        """
3892        Method for obtaining the available funds for withdrawal for current `accountId`.
3893
3894        See also:
3895        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3896        - `OverviewLimits()` method
3897
3898        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3899                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3900                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3901                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3902        """
3903        if self.accountId is None or not self.accountId:
3904            uLogger.error("Variable `accountId` must be defined for using this method!")
3905            raise Exception("Account ID required")
3906
3907        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3908
3909        self.body = str({"accountId": self.accountId})
3910        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3911        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3912
3913        if self.moreDebug:
3914            uLogger.debug("Records about available funds for withdrawal successfully received")
3915
3916        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3918    def OverviewLimits(self, show: bool = False, onlyFiles=False) -> dict:
3919        """
3920        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3921
3922        See also: `RequestLimits()`.
3923
3924        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3925        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
3926        :return: dict with raw parsed data from server and some calculated statistics about it.
3927        """
3928        if self.accountId is None or not self.accountId:
3929            uLogger.error("Variable `accountId` must be defined for using this method!")
3930            raise Exception("Account ID required")
3931
3932        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3933
3934        view = {
3935            "rawLimits": rawLimits,
3936            "limits": {  # parsed data for every currency:
3937                "money": {  # this is an array of portfolio currency positions
3938                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3939                },
3940                "blocked": {  # this is an array of blocked currency
3941                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3942                },
3943                "blockedGuarantee": {  # this is locked money under collateral for futures
3944                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3945                },
3946            },
3947        }
3948
3949        # --- Prepare text table with limits in human-readable format:
3950        if show or onlyFiles:
3951            info = [
3952                "# Withdrawal limits\n\n",
3953                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3954                "* **Account ID:** [{}]\n".format(self.accountId),
3955            ]
3956
3957            if view["limits"]["money"]:
3958                info.extend([
3959                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3960                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3961                ])
3962
3963            else:
3964                info.append("\nNo withdrawal limits\n")
3965
3966            for curr in view["limits"]["money"].keys():
3967                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3968                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3969                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3970
3971                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3972                    "[{}]".format(curr),
3973                    "{:.2f}".format(view["limits"]["money"][curr]),
3974                    "{:.2f}".format(availableMoney),
3975                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3976                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3977                )
3978
3979                if curr == "rub":
3980                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3981
3982                else:
3983                    info.append(infoStr)
3984
3985            infoText = "".join(info)
3986
3987            if show and not onlyFiles:
3988                uLogger.info(infoText)
3989
3990            if self.withdrawalLimitsFile and (show or onlyFiles):
3991                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3992                    fH.write(infoText)
3993
3994                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3995
3996                if self.useHTMLReports:
3997                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3998                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3999                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
4000
4001                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4002
4003        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
4005    def RequestAccounts(self) -> dict:
4006        """
4007        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
4008
4009        See also:
4010        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
4011        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4012        - `OverviewUserInfo()` method
4013
4014        :return: dict with raw data from server that contains accounts info. Example of dict:
4015                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4016                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4017                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4018                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4019        """
4020        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4021
4022        self.body = str({})
4023        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4024        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4025
4026        if self.moreDebug:
4027            uLogger.debug("Records about available accounts successfully received")
4028
4029        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
4031    def RequestUserInfo(self) -> dict:
4032        """
4033        Method for requesting common user's information.
4034
4035        See also:
4036        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4037        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4038        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4039        - `OverviewUserInfo()` method
4040
4041        :return: dict with raw data from server that contains user's information. Example of dict:
4042                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4043                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4044        """
4045        uLogger.debug("Requesting common user's information. Wait, please...")
4046
4047        self.body = str({})
4048        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4049        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4050
4051        if self.moreDebug:
4052            uLogger.debug("Records about current user successfully received")
4053
4054        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
4056    def RequestMarginStatus(self, accountId: str = None) -> dict:
4057        """
4058        Method for requesting margin calculation for defined account ID.
4059
4060        See also:
4061        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4062        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4063        - `OverviewUserInfo()` method
4064
4065        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4066        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4067                 Example of responses:
4068                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4069                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4070                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4071                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4072                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4073                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4074        """
4075        if accountId is None or not accountId:
4076            if self.accountId is None or not self.accountId:
4077                uLogger.error("Variable `accountId` must be defined for using this method!")
4078                raise Exception("Account ID required")
4079
4080            else:
4081                accountId = self.accountId  # use `self.accountId` (main ID) by default
4082
4083        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4084
4085        self.body = str({"accountId": accountId})
4086        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4087        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4088
4089        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4090            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4091            rawMargin = {}
4092
4093        else:
4094            if self.moreDebug:
4095                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4096
4097        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4099    def RequestTariffLimits(self) -> dict:
4100        """
4101        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4102
4103        See also:
4104        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4105        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4106        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4107        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4108        - `OverviewUserInfo()` method
4109
4110        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4111                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4112                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4113        """
4114        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4115
4116        self.body = str({})
4117        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4118        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4119
4120        if self.moreDebug:
4121            uLogger.debug("Records with limits of current tariff successfully received")
4122
4123        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4125    def RequestBondCoupons(self, iJSON: dict) -> dict:
4126        """
4127        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4128        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4129        All dates are in UTC timezone.
4130
4131        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4132        Documentation:
4133        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4134        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4135
4136        See also: `ExtendBondsData()`.
4137
4138        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4139                      If raw iJSON is not data of bond then server returns an error [400] with message:
4140                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4141        :return: dictionary with bond payment calendar. Response example
4142                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4143                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4144                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4145                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4146        """
4147        if iJSON["figi"] is None or not iJSON["figi"]:
4148            uLogger.error("FIGI must be defined for using this method!")
4149            raise Exception("FIGI required")
4150
4151        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4152        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4153
4154        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4155            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4156            self._figi,
4157            startDate,
4158            endDate,
4159        ))
4160
4161        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4162        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4163        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4164
4165        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4166            uLogger.warning("Instrument type is not bond!")
4167
4168        else:
4169            if self.moreDebug:
4170                uLogger.debug("Records about bond payment calendar successfully received")
4171
4172        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4174    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4175        """
4176        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4177        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4178        coupon yields, current yields and some statistics etc.
4179
4180        WARNING! This is too long operation if a lot of bonds requested from broker server.
4181
4182        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4183
4184        :param instruments: list of strings with tickers or FIGIs.
4185        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4186                     for further used by data scientists or stock analytics.
4187        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4188                 In XLSX-file and Pandas DataFrame fields mean:
4189                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4190                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4191        """
4192        if instruments is None or not instruments:
4193            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4194            raise Exception("Ticker or FIGI required")
4195
4196        if isinstance(instruments, str):
4197            instruments = [instruments]
4198
4199        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4200
4201        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4202
4203        iCount = len(uniqueInstruments)
4204        tooLong = iCount >= 20
4205        if tooLong:
4206            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4207
4208        bonds = None
4209        for i, self._figi in enumerate(uniqueInstruments):
4210            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4211
4212            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4213                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4214                rawBond = self.SearchByFIGI(requestPrice=True)
4215
4216                # Widen raw data with UTC current time (iData["actualDateTime"]):
4217                actualDate = datetime.now(tzutc())
4218                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4219
4220                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4221                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4222
4223                # Replace some values with human-readable:
4224                iData["nominalCurrency"] = iData["nominal"]["currency"]
4225                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4226                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4227                iData["aciCurrency"] = iData["aciValue"]["currency"]
4228                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4229                iData["issueSize"] = int(iData["issueSize"])
4230                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4231                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4232                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4233                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4234                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4235                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4236                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4237                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4238                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4239                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4240
4241                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4242                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4243                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4244                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4245                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4246                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4247                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4248                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4249                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4250                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4251                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4252
4253                # Widen raw data with calendar data from `rawCalendar` values:
4254                calendarData = []
4255                if "events" in iData["rawCalendar"].keys():
4256                    for item in iData["rawCalendar"]["events"]:
4257                        calendarData.append({
4258                            "couponDate": item["couponDate"],
4259                            "couponNumber": int(item["couponNumber"]),
4260                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4261                            "payCurrency": item["payOneBond"]["currency"],
4262                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4263                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4264                            "couponStartDate": item["couponStartDate"],
4265                            "couponEndDate": item["couponEndDate"],
4266                            "couponPeriod": item["couponPeriod"],
4267                        })
4268
4269                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4270                    if "maturityDate" not in iData.keys():
4271                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4272
4273                # Widen raw data with Coupon Rate.
4274                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4275                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4276                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4277                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4278
4279                # Widen raw data with Yield to Maturity (YTM) on current date.
4280                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4281                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4282                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4283                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4284                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4285                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4286
4287                iData["calendar"] = calendarData  # adds calendar at the end
4288
4289                # Remove not used data:
4290                iData.pop("uid")
4291                iData.pop("positionUid")
4292                iData.pop("currentPrice")
4293                iData.pop("rawCalendar")
4294
4295                colNames = list(iData.keys())
4296                if bonds is None:
4297                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4298
4299                else:
4300                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4301
4302            else:
4303                uLogger.warning("Instrument is not a bond!")
4304
4305            processed = round(100 * (i + 1) / iCount, 1)
4306            if tooLong and processed % 5 == 0:
4307                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4308
4309            else:
4310                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4311
4312        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4313
4314        # Saving bonds from Pandas DataFrame to XLSX sheet:
4315        if xlsx and self.bondsXLSXFile:
4316            with pd.ExcelWriter(
4317                    path=self.bondsXLSXFile,
4318                    date_format=TKS_DATE_FORMAT,
4319                    datetime_format=TKS_DATE_TIME_FORMAT,
4320                    mode="w",
4321            ) as writer:
4322                bonds.to_excel(
4323                    writer,
4324                    sheet_name="Extended bonds data",
4325                    index=True,
4326                    encoding="UTF-8",
4327                    freeze_panes=(1, 1),
4328                )  # saving as XLSX-file with freeze first row and column as headers
4329
4330            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4331
4332        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4334    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4335        """
4336        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4337
4338        WARNING! This is too long operation if a lot of bonds requested from broker server.
4339
4340        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4341
4342        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4343                        extended information about bonds: main info, current prices, bond payment calendar,
4344                        coupon yields, current yields and some statistics etc.
4345                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4346        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4347                     for further used by data scientists or stock analytics.
4348        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4349        """
4350        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4351            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4352
4353        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4354
4355        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4356        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4357        calendar = None
4358        for bond in extBonds.iterrows():
4359            for item in bond[1]["calendar"]:
4360                cData = {
4361                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4362                    "couponDate": item["couponDate"],
4363                    "figi": bond[1]["figi"],
4364                    "ticker": bond[1]["ticker"],
4365                    "name": bond[1]["name"],
4366                    "couponNumber": item["couponNumber"],
4367                    "payOneBond": item["payOneBond"],
4368                    "payCurrency": item["payCurrency"],
4369                    "couponType": item["couponType"],
4370                    "couponPeriod": item["couponPeriod"],
4371                    "fixDate": item["fixDate"],
4372                    "couponStartDate": item["couponStartDate"],
4373                    "couponEndDate": item["couponEndDate"],
4374                }
4375
4376                if calendar is None:
4377                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4378
4379                else:
4380                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4381
4382        if calendar is not None:
4383            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4384
4385            # Saving calendar from Pandas DataFrame to XLSX sheet:
4386            if xlsx:
4387                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4388
4389                with pd.ExcelWriter(
4390                        path=xlsxCalendarFile,
4391                        date_format=TKS_DATE_FORMAT,
4392                        datetime_format=TKS_DATE_TIME_FORMAT,
4393                        mode="w",
4394                ) as writer:
4395                    humanReadable = calendar.copy(deep=True)
4396                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4397                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4398                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4399                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4400                    humanReadable.columns = colNames  # human-readable column names
4401
4402                    humanReadable.to_excel(
4403                        writer,
4404                        sheet_name="Bond payments calendar",
4405                        index=False,
4406                        encoding="UTF-8",
4407                        freeze_panes=(1, 2),
4408                    )  # saving as XLSX-file with freeze first row and column as headers
4409
4410                    del humanReadable  # release df in memory
4411
4412                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4413
4414        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, show: bool = True, onlyFiles=False) -> str:
4416    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True, onlyFiles=False) -> str:
4417        """
4418        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4419        Also, creates Markdown file with calendar data, `calendar.md` by default.
4420
4421        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4422
4423        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4424                        extended information about bonds: main info, current prices, bond payment calendar,
4425                        coupon yields, current yields and some statistics etc.
4426                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4427        :param show: if `True` then also printing bonds payment calendar to the console,
4428                     otherwise save to file `calendarFile` only. `False` by default.
4429        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4430        :return: multilines text in Markdown format with bonds payment calendar as a table.
4431        """
4432        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4433            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=show or onlyFiles)
4434
4435        infoText = "# Bond payments calendar\n\n"
4436
4437        calendar = self.CreateBondsCalendar(extBonds, xlsx=show or onlyFiles)  # generate Pandas DataFrame with full calendar data
4438
4439        if not (calendar is None or calendar.empty):
4440            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4441
4442            info = [
4443                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4444                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4445                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4446            ]
4447
4448            newMonth = False
4449            notOneBond = calendar["figi"].nunique() > 1
4450            for i, bond in enumerate(calendar.iterrows()):
4451                if newMonth and notOneBond:
4452                    info.append(splitLine)
4453
4454                info.append(
4455                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4456                        "  √" if bond[1]["paid"] else "  —",
4457                        bond[1]["couponDate"].split("T")[0],
4458                        bond[1]["figi"],
4459                        bond[1]["ticker"],
4460                        bond[1]["couponNumber"],
4461                        "{} {}".format(
4462                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4463                            bond[1]["payCurrency"],
4464                        ),
4465                        bond[1]["couponType"],
4466                        bond[1]["couponPeriod"],
4467                        bond[1]["fixDate"].split("T")[0],
4468                    )
4469                )
4470
4471                if i < len(calendar.values) - 1:
4472                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4473                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4474                    newMonth = False if curDate.month == nextDate.month else True
4475
4476                else:
4477                    newMonth = False
4478
4479            infoText += "".join(info)
4480
4481            if show and not onlyFiles:
4482                uLogger.info("{}".format(infoText))
4483
4484            if self.calendarFile is not None and (show or onlyFiles):
4485                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4486                    fH.write(infoText)
4487
4488                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4489
4490                if self.useHTMLReports:
4491                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4492                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4493                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4494
4495                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4496
4497        else:
4498            infoText += "No data\n"
4499
4500        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4502    def OverviewAccounts(self, show: bool = False, onlyFiles=False) -> dict:
4503        """
4504        Method for parsing and show simple table with all available user accounts.
4505
4506        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4507
4508        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4509        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4510        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4511                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4512                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4513                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4514                                                        "closed": "—", "access": "Full access" }, ...}}`
4515        """
4516        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4517
4518        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4519        accounts = {
4520            item["id"]: {
4521                "type": TKS_ACCOUNT_TYPES[item["type"]],
4522                "name": item["name"],
4523                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4524                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4525                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4526                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4527            } for item in rawAccounts["accounts"]
4528        }
4529
4530        # Raw and parsed data with some fields replaced in "stat" section:
4531        view = {
4532            "rawAccounts": rawAccounts,
4533            "stat": accounts,
4534        }
4535
4536        # --- Prepare simple text table with only accounts data in human-readable format:
4537        if show or onlyFiles:
4538            info = [
4539                "# User accounts\n\n",
4540                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4541                "| Account ID   | Type                      | Status                    | Name                           |\n",
4542                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4543            ]
4544
4545            for account in view["stat"].keys():
4546                info.extend([
4547                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4548                        account,
4549                        view["stat"][account]["type"],
4550                        view["stat"][account]["status"],
4551                        view["stat"][account]["name"],
4552                    )
4553                ])
4554
4555            infoText = "".join(info)
4556
4557            if show and not onlyFiles:
4558                uLogger.info(infoText)
4559
4560            if self.userAccountsFile and (show or onlyFiles):
4561                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4562                    fH.write(infoText)
4563
4564                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4565
4566                if self.useHTMLReports:
4567                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4568                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4569                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4570
4571                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4572
4573        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4575    def OverviewUserInfo(self, show: bool = False, onlyFiles=False) -> dict:
4576        """
4577        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4578
4579        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4580
4581        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4582        :param onlyFiles: if `True` then do not show Markdown table in the console, but only generates report files.
4583        :return: dict with raw parsed data from server and some calculated statistics about it.
4584        """
4585        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4586        tmpTicker = self._ticker
4587        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4588        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4589        self._ticker = tmpTicker
4590
4591        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4592        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4593        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4594        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4595        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4596        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4597
4598        # This is dict with parsed common user data:
4599        userInfo = {
4600            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4601            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4602            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4603            "tariff": rawUserInfo["tariff"],
4604        }
4605
4606        # This is an array of dict with parsed margin statuses for every account IDs:
4607        margins = {}
4608        for accountId in accounts.keys():
4609            if rawMargins[accountId]:
4610                margins[accountId] = {
4611                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4612                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4613                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4614                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4615                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4616                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4617                    "missing": missing["volume"],
4618                }
4619
4620            else:
4621                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4622
4623        unary = {}  # unary-connection limits
4624        for item in rawTariffLimits["unaryLimits"]:
4625            if item["limitPerMinute"] in unary.keys():
4626                unary[item["limitPerMinute"]].extend(item["methods"])
4627
4628            else:
4629                unary[item["limitPerMinute"]] = item["methods"]
4630
4631        stream = {}  # stream-connection limits
4632        for item in rawTariffLimits["streamLimits"]:
4633            if item["limit"] in stream.keys():
4634                stream[item["limit"]].extend(item["streams"])
4635
4636            else:
4637                stream[item["limit"]] = item["streams"]
4638
4639        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4640        limits = {
4641            "unary": unary,
4642            "stream": stream,
4643        }
4644
4645        # Raw and parsed data as an output result:
4646        view = {
4647            "rawUserInfo": rawUserInfo,
4648            "rawAccounts": rawAccounts,
4649            "rawMargins": rawMargins,
4650            "rawTariffLimits": rawTariffLimits,
4651            "stat": {
4652                "overview": overview,
4653                "userInfo": userInfo,
4654                "accounts": accounts,
4655                "margins": margins,
4656                "limits": limits,
4657            },
4658        }
4659
4660        # --- Prepare text table with user information in human-readable format:
4661        if show or onlyFiles:
4662            info = [
4663                "# Full user information\n\n",
4664                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4665                "## Common information\n\n",
4666                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4667                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4668                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4669                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4670                "\n## User accounts\n\n",
4671            ]
4672
4673            for account in view["stat"]["accounts"].keys():
4674                info.extend([
4675                    "### ID: [{}]\n\n".format(account),
4676                    "| Parameters           | Values                                                       |\n",
4677                    "|----------------------|--------------------------------------------------------------|\n",
4678                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4679                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4680                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4681                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4682                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4683                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4684                ])
4685
4686                if margins[account]:
4687                    info.extend([
4688                        "| Margin status:       | Enabled                                                      |\n",
4689                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4690                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4691                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4692                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4693                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4694                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4695                    ])
4696
4697                else:
4698                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4699
4700            info.extend([
4701                "\n## Current user tariff limits\n",
4702                "\n### See also\n",
4703                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4704                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4705                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4706                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4707                "\n### Unary limits\n",
4708            ])
4709
4710            if unary:
4711                for key, values in sorted(unary.items()):
4712                    info.append("\n* Max requests per minute: {}\n".format(key))
4713
4714                    for value in values:
4715                        info.append("  - {}\n".format(value))
4716
4717            else:
4718                info.append("\nNot available\n")
4719
4720            info.append("\n### Stream limits\n")
4721
4722            if stream:
4723                for key, values in sorted(stream.items()):
4724                    info.append("\n* Max stream connections: {}\n".format(key))
4725
4726                    for value in values:
4727                        info.append("  - {}\n".format(value))
4728
4729            else:
4730                info.append("\nNot available\n")
4731
4732            infoText = "".join(info)
4733
4734            if show and not onlyFiles:
4735                uLogger.info(infoText)
4736
4737            if self.userInfoFile and (show or onlyFiles):
4738                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4739                    fH.write(infoText)
4740
4741                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4742
4743                if self.useHTMLReports:
4744                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4745                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4746                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4747
4748                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4749
4750        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
  • onlyFiles: if True then do not show Markdown table in the console, but only generates report files.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4753class Args:
4754    """
4755    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4756    """
4757    def __init__(self, **kwargs):
4758        self.__dict__.update(kwargs)
4759
4760    def __getattr__(self, item):
4761        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4757    def __init__(self, **kwargs):
4758        self.__dict__.update(kwargs)
def ParseArgs():
4764def ParseArgs():
4765    """This function get and parse command line keys."""
4766    parser = ArgumentParser()  # command-line string parser
4767
4768    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4769    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4770
4771    # --- options:
4772
4773    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4774    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4775    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4776
4777    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4778    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4779
4780    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4781    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4782
4783    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4784    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4785
4786    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4787    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4788    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4789
4790    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4791    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4792
4793    # --- commands:
4794
4795    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4796
4797    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4798    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4799    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4800    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4801    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4802    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4803    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4804    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4805
4806    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4807    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4808    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4809    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4810    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4811    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4812
4813    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4814    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4815    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4816    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4817
4818    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4819    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4820    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4821
4822    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4823    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4824    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4825    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4826    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4827    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4828    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4829
4830    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4831    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4832    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4833    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4834    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4835
4836    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4837    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4838    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4839
4840    cmdArgs = parser.parse_args()
4841    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs):
4844def Main(**kwargs):
4845    """
4846    Main function for work with TKSBrokerAPI in the console.
4847
4848    See examples:
4849    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4850    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4851    """
4852    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4853
4854    if args.debug_level:
4855        uLogger.level = 10  # always debug level by default
4856        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4857
4858    exitCode = 0
4859    start = datetime.now(tzutc())
4860    uLogger.debug("=-" * 50)
4861    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4862        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4863        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4864    ))
4865
4866    # trying to calculate full current version:
4867    buildVersion = __version__
4868    try:
4869        v = version("tksbrokerapi")
4870        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4871
4872    except Exception:
4873        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4874
4875    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4876    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4877
4878    try:
4879        if args.version:
4880            print("TKSBrokerAPI {}".format(buildVersion))
4881            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4882
4883        else:
4884            # Init class for trading with Tinkoff Broker:
4885            trader = TinkoffBrokerServer(
4886                token=args.token,
4887                accountId=args.account_id,
4888                useCache=not args.no_cache,
4889            )
4890
4891            # --- set some options:
4892
4893            if args.more:
4894                trader.moreDebug = True
4895                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4896
4897            if args.html:
4898                trader.useHTMLReports = True
4899
4900            if args.ticker:
4901                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4902
4903                if ticker in trader.aliasesKeys:
4904                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4905
4906                else:
4907                    trader.ticker = ticker
4908
4909            if args.figi:
4910                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4911
4912            if args.depth is not None:
4913                trader.depth = args.depth
4914
4915            # --- do one command:
4916
4917            if args.list:
4918                if args.output is not None:
4919                    trader.instrumentsFile = args.output
4920
4921                trader.ShowInstrumentsInfo(show=True)
4922
4923            elif args.list_xlsx:
4924                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4925
4926            elif args.bonds_xlsx is not None:
4927                if args.output is not None:
4928                    trader.bondsXLSXFile = args.output
4929
4930                if len(args.bonds_xlsx) == 0:
4931                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4932
4933                else:
4934                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4935
4936            elif args.search:
4937                if args.output is not None:
4938                    trader.searchResultsFile = args.output
4939
4940                trader.SearchInstruments(pattern=args.search[0], show=True)
4941
4942            elif args.info:
4943                if not (args.ticker or args.figi):
4944                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4945                    raise Exception("Ticker or FIGI required")
4946
4947                if args.output is not None:
4948                    trader.infoFile = args.output
4949
4950                if args.ticker:
4951                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4952
4953                else:
4954                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4955
4956            elif args.calendar is not None:
4957                if args.output is not None:
4958                    trader.calendarFile = args.output
4959
4960                if len(args.calendar) == 0:
4961                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4962
4963                else:
4964                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4965
4966                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4967
4968            elif args.price:
4969                if not (args.ticker or args.figi):
4970                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4971                    raise Exception("Ticker or FIGI required")
4972
4973                trader.GetCurrentPrices(show=True)
4974
4975            elif args.prices is not None:
4976                if args.output is not None:
4977                    trader.pricesFile = args.output
4978
4979                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4980
4981            elif args.overview:
4982                if args.output is not None:
4983                    trader.overviewFile = args.output
4984
4985                trader.Overview(show=True, details="full")
4986
4987            elif args.overview_digest:
4988                if args.output is not None:
4989                    trader.overviewDigestFile = args.output
4990
4991                trader.Overview(show=True, details="digest")
4992
4993            elif args.overview_positions:
4994                if args.output is not None:
4995                    trader.overviewPositionsFile = args.output
4996
4997                trader.Overview(show=True, details="positions")
4998
4999            elif args.overview_orders:
5000                if args.output is not None:
5001                    trader.overviewOrdersFile = args.output
5002
5003                trader.Overview(show=True, details="orders")
5004
5005            elif args.overview_analytics:
5006                if args.output is not None:
5007                    trader.overviewAnalyticsFile = args.output
5008
5009                trader.Overview(show=True, details="analytics")
5010
5011            elif args.overview_calendar:
5012                if args.output is not None:
5013                    trader.overviewAnalyticsFile = args.output
5014
5015                trader.Overview(show=True, details="calendar")
5016
5017            elif args.deals is not None:
5018                if args.output is not None:
5019                    trader.reportFile = args.output
5020
5021                if 0 <= len(args.deals) < 3:
5022                    trader.Deals(
5023                        start=args.deals[0] if len(args.deals) >= 1 else None,
5024                        end=args.deals[1] if len(args.deals) == 2 else None,
5025                        show=True,  # Always show deals report in console
5026                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5027                    )
5028
5029                else:
5030                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5031                    raise Exception("Incorrect value")
5032
5033            elif args.history is not None:
5034                if args.output is not None:
5035                    trader.historyFile = args.output
5036
5037                if 0 <= len(args.history) < 3:
5038                    dataReceived = trader.History(
5039                        start=args.history[0] if len(args.history) >= 1 else None,
5040                        end=args.history[1] if len(args.history) == 2 else None,
5041                        interval="hour" if args.interval is None or not args.interval else args.interval,
5042                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5043                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5044                        show=True,  # shows all downloaded candles in console
5045                    )
5046
5047                    if args.render_chart is not None and dataReceived is not None:
5048                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5049
5050                        trader.ShowHistoryChart(
5051                            candles=dataReceived,
5052                            interact=iChart,
5053                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5054                        )
5055
5056                else:
5057                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5058                    raise Exception("Incorrect value")
5059
5060            elif args.load_history is not None:
5061                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5062
5063                if args.render_chart is not None and histData is not None:
5064                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5065                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5066
5067                    trader.ShowHistoryChart(
5068                        candles=histData,
5069                        interact=iChart,
5070                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5071                    )
5072
5073            elif args.trade is not None:
5074                if 1 <= len(args.trade) <= 5:
5075                    trader.Trade(
5076                        operation=args.trade[0],
5077                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5078                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5079                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5080                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5081                    )
5082
5083                else:
5084                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5085
5086            elif args.buy is not None:
5087                if 0 <= len(args.buy) <= 4:
5088                    trader.Buy(
5089                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5090                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5091                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5092                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5093                    )
5094
5095                else:
5096                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5097
5098            elif args.sell is not None:
5099                if 0 <= len(args.sell) <= 4:
5100                    trader.Sell(
5101                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5102                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5103                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5104                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5105                    )
5106
5107                else:
5108                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5109
5110            elif args.order:
5111                if 4 <= len(args.order) <= 7:
5112                    trader.Order(
5113                        operation=args.order[0],
5114                        orderType=args.order[1],
5115                        lots=int(args.order[2]),
5116                        targetPrice=float(args.order[3]),
5117                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5118                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5119                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5120                    )
5121
5122                else:
5123                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5124
5125            elif args.buy_limit:
5126                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5127
5128            elif args.sell_limit:
5129                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5130
5131            elif args.buy_stop:
5132                if 2 <= len(args.buy_stop) <= 7:
5133                    trader.BuyStop(
5134                        lots=int(args.buy_stop[0]),
5135                        targetPrice=float(args.buy_stop[1]),
5136                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5137                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5138                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5139                    )
5140
5141                else:
5142                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5143
5144            elif args.sell_stop:
5145                if 2 <= len(args.sell_stop) <= 7:
5146                    trader.SellStop(
5147                        lots=int(args.sell_stop[0]),
5148                        targetPrice=float(args.sell_stop[1]),
5149                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5150                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5151                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5152                    )
5153
5154                else:
5155                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5156
5157            # elif args.buy_order_grid is not None:
5158            #     # update order grid work with api v2
5159            #     if len(args.buy_order_grid) == 2:
5160            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5161            #
5162            #         for order in orderParams:
5163            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5164            #
5165            #     else:
5166            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5167            #
5168            # elif args.sell_order_grid is not None:
5169            #     # update order grid work with api v2
5170            #     if len(args.sell_order_grid) >= 2:
5171            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5172            #
5173            #         for order in orderParams:
5174            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5175            #
5176            #     else:
5177            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5178
5179            elif args.close_order is not None:
5180                trader.CloseOrders(args.close_order)  # close only one order
5181
5182            elif args.close_orders is not None:
5183                trader.CloseOrders(args.close_orders)  # close list of orders
5184
5185            elif args.close_trade:
5186                if not (args.ticker or args.figi):
5187                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5188                    raise Exception("Ticker or FIGI required")
5189
5190                if args.ticker:
5191                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5192
5193                else:
5194                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5195
5196            elif args.close_trades is not None:
5197                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5198
5199            elif args.close_all is not None:
5200                if args.ticker:
5201                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5202
5203                elif args.figi:
5204                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5205
5206                else:
5207                    trader.CloseAll(*args.close_all)
5208
5209            elif args.limits:
5210                if args.output is not None:
5211                    trader.withdrawalLimitsFile = args.output
5212
5213                trader.OverviewLimits(show=True)
5214
5215            elif args.user_info:
5216                if args.output is not None:
5217                    trader.userInfoFile = args.output
5218
5219                trader.OverviewUserInfo(show=True)
5220
5221            elif args.account:
5222                if args.output is not None:
5223                    trader.userAccountsFile = args.output
5224
5225                trader.OverviewAccounts(show=True)
5226
5227            else:
5228                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5229                raise Exception("There is no command to execute")
5230
5231    except Exception:
5232        trace = tb.format_exc()
5233        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5234            if e in trace:
5235                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5236                break
5237
5238        uLogger.debug(trace)
5239        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5240        exitCode = 255  # an error occurred, must be open a ticket for this issue
5241
5242    finally:
5243        finish = datetime.now(tzutc())
5244
5245        if exitCode == 0:
5246            if args.more:
5247                uLogger.debug("All operations were finished success (summary code is 0).")
5248
5249        else:
5250            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5251                os.path.abspath(uLog.defaultLogFile), exitCode,
5252            ))
5253
5254        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5255        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5256            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5257            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5258        ))
5259        uLogger.debug("=-" * 50)
5260
5261        if not kwargs:
5262            sys.exit(exitCode)
5263
5264        else:
5265            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: